@baasix/baasix 0.1.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 (666) hide show
  1. package/LICENSE.MD +85 -0
  2. package/README.md +526 -0
  3. package/assets/banner.jpg +0 -0
  4. package/assets/banner_small.jpg +0 -0
  5. package/assets/logo_icon.svg +20 -0
  6. package/assets/logo_icon_rounded.svg +20 -0
  7. package/dist/LICENSE.MD +85 -0
  8. package/dist/README.md +526 -0
  9. package/dist/app/404/index.html +1 -0
  10. package/dist/app/404.html +1 -0
  11. package/dist/app/_next/static/chunks/041e1f03-56ae8a902a7f2fe6.js +24 -0
  12. package/dist/app/_next/static/chunks/1117-05479929a8da73e3.js +1 -0
  13. package/dist/app/_next/static/chunks/1299.77cc7b7b76b75cba.js +1 -0
  14. package/dist/app/_next/static/chunks/1303-35a96e9c9cdeab9d.js +1 -0
  15. package/dist/app/_next/static/chunks/1509-56ac00cdaaecdf53.js +1 -0
  16. package/dist/app/_next/static/chunks/1668-e3eabd0f6753c780.js +1 -0
  17. package/dist/app/_next/static/chunks/1783-d9fb550fd324300c.js +1 -0
  18. package/dist/app/_next/static/chunks/2117-29b5fa47421595ad.js +2 -0
  19. package/dist/app/_next/static/chunks/2344.35b46d2179a765b5.js +1 -0
  20. package/dist/app/_next/static/chunks/257.990da16794a31292.js +1 -0
  21. package/dist/app/_next/static/chunks/2676-73b0ee7c80073a84.js +1 -0
  22. package/dist/app/_next/static/chunks/3563-b8842744384391fe.js +1 -0
  23. package/dist/app/_next/static/chunks/363642f4-933b579ed3c85f60.js +1 -0
  24. package/dist/app/_next/static/chunks/3817-e20c8f0a0810fc95.js +1 -0
  25. package/dist/app/_next/static/chunks/3834.84944e390d902509.js +2 -0
  26. package/dist/app/_next/static/chunks/4043-3a30c8a75896f241.js +1 -0
  27. package/dist/app/_next/static/chunks/4225-14090c7c0cd9dec6.js +1 -0
  28. package/dist/app/_next/static/chunks/4438-c9a12ca15b6e9160.js +1 -0
  29. package/dist/app/_next/static/chunks/4458-679fd0c6884f456a.js +1 -0
  30. package/dist/app/_next/static/chunks/4475-8bdfbd536fba8c48.js +1 -0
  31. package/dist/app/_next/static/chunks/4883-8a924721bb21b3b0.js +1 -0
  32. package/dist/app/_next/static/chunks/489-683ab07188f9df2b.js +1 -0
  33. package/dist/app/_next/static/chunks/4952-1b97320cf61f3f21.js +1 -0
  34. package/dist/app/_next/static/chunks/5094-8d53e403235d4ca6.js +1 -0
  35. package/dist/app/_next/static/chunks/5101-3a146e0625747ad1.js +1 -0
  36. package/dist/app/_next/static/chunks/54a60aa6-d9747982e0a81f58.js +79 -0
  37. package/dist/app/_next/static/chunks/5650-f096291df402bfc2.js +1 -0
  38. package/dist/app/_next/static/chunks/600-539045311240f579.js +1 -0
  39. package/dist/app/_next/static/chunks/6170-803b82e19d3ade6d.js +89 -0
  40. package/dist/app/_next/static/chunks/6241-30d7169d1010e5a4.js +1 -0
  41. package/dist/app/_next/static/chunks/6530-a91e10cffa4200c4.js +1 -0
  42. package/dist/app/_next/static/chunks/6547-4bbbdb5c399aef1e.js +1 -0
  43. package/dist/app/_next/static/chunks/6712-781937c53a2c49da.js +1 -0
  44. package/dist/app/_next/static/chunks/6fcbdc68-90be1a5480b8d353.js +1 -0
  45. package/dist/app/_next/static/chunks/70e0d97a-aeaf0cdc26ba1a58.js +1 -0
  46. package/dist/app/_next/static/chunks/7214-5154a89d08d24dde.js +1 -0
  47. package/dist/app/_next/static/chunks/7324-b53229c59a640880.js +10 -0
  48. package/dist/app/_next/static/chunks/7636-66424f0b51d350e9.js +1 -0
  49. package/dist/app/_next/static/chunks/7874-39a3f2541165a675.js +1 -0
  50. package/dist/app/_next/static/chunks/7982-9da12b83f11e3f5f.js +1 -0
  51. package/dist/app/_next/static/chunks/8213a2eb-da25a3b3c5521b2b.js +1 -0
  52. package/dist/app/_next/static/chunks/8473-6598318371eca31b.js +1 -0
  53. package/dist/app/_next/static/chunks/8640fa6b-72e43370f68e5587.js +1 -0
  54. package/dist/app/_next/static/chunks/9090-3ef676f29c95f1c7.js +1 -0
  55. package/dist/app/_next/static/chunks/9124-a02f9e209e6e3cce.js +1 -0
  56. package/dist/app/_next/static/chunks/926-156f32067d111d6b.js +1 -0
  57. package/dist/app/_next/static/chunks/9487-b17481605e513b83.js +1 -0
  58. package/dist/app/_next/static/chunks/9599-a7e572bb88c3392b.js +1 -0
  59. package/dist/app/_next/static/chunks/9881-419697138376e755.js +1 -0
  60. package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/all-activity/page-8917930b4d663405.js +1 -0
  61. package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/email-log/page-b27a6ee32782d7df.js +1 -0
  62. package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/notifications/page-b7eda523ede2702c.js +1 -0
  63. package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/page-1cfa62d1caedaed0.js +1 -0
  64. package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/sessions/page-3e21e20db90aeff7.js +1 -0
  65. package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/workflow-executions/page-27bcc26b747fb29b.js +1 -0
  66. package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/workflow-logs/page-9f9e9e952aef436e.js +1 -0
  67. package/dist/app/_next/static/chunks/app/(authenticated)/change-password/page-8d61aa499eabb127.js +1 -0
  68. package/dist/app/_next/static/chunks/app/(authenticated)/dashboard/page-1ceeac9e72997a8a.js +1 -0
  69. package/dist/app/_next/static/chunks/app/(authenticated)/data-browser/page-8cda2b57759dd670.js +1 -0
  70. package/dist/app/_next/static/chunks/app/(authenticated)/file-manager/page-8c6f1b1da66ad7e4.js +1 -0
  71. package/dist/app/_next/static/chunks/app/(authenticated)/layout-f70d225b2759c998.js +1 -0
  72. package/dist/app/_next/static/chunks/app/(authenticated)/settings/migrations/page-aacec8f7cfb40ab2.js +1 -0
  73. package/dist/app/_next/static/chunks/app/(authenticated)/settings/permissions/page-828110cfcde429c6.js +1 -0
  74. package/dist/app/_next/static/chunks/app/(authenticated)/settings/project/page-420e794bb76bd204.js +1 -0
  75. package/dist/app/_next/static/chunks/app/(authenticated)/settings/roles/page-9001d02b28f70708.js +1 -0
  76. package/dist/app/_next/static/chunks/app/(authenticated)/settings/schema/page-899574f35091dd58.js +1 -0
  77. package/dist/app/_next/static/chunks/app/(authenticated)/settings/tasks/page-ad7ab3e27c83f44f.js +1 -0
  78. package/dist/app/_next/static/chunks/app/(authenticated)/settings/templates/edit/page-bd83414cb8c4cb04.js +1 -0
  79. package/dist/app/_next/static/chunks/app/(authenticated)/settings/templates/page-3181447f8772b1d3.js +1 -0
  80. package/dist/app/_next/static/chunks/app/(authenticated)/settings/tenants/page-ef9bfbacef5a1d73.js +1 -0
  81. package/dist/app/_next/static/chunks/app/(authenticated)/users/invites/page-480306b7b2bbac7e.js +1 -0
  82. package/dist/app/_next/static/chunks/app/(authenticated)/users/list/page-74da51254c2606b3.js +1 -0
  83. package/dist/app/_next/static/chunks/app/(authenticated)/users/page-e99c6f0b915001b2.js +1 -0
  84. package/dist/app/_next/static/chunks/app/(authenticated)/users/preferences/page-1a935630ce8f2b12.js +1 -0
  85. package/dist/app/_next/static/chunks/app/(authenticated)/users/user-roles/page-901dfb8ea1f39ca8.js +1 -0
  86. package/dist/app/_next/static/chunks/app/(authenticated)/workflows/detail/page-9a6b839aea688ca4.js +1 -0
  87. package/dist/app/_next/static/chunks/app/(authenticated)/workflows/edit/page-11774efbc2fecae2.js +1 -0
  88. package/dist/app/_next/static/chunks/app/(authenticated)/workflows/execution/page-8ec1aea90412c03d.js +1 -0
  89. package/dist/app/_next/static/chunks/app/(authenticated)/workflows/page-88bc5b36ccb0a1f7.js +1 -0
  90. package/dist/app/_next/static/chunks/app/(public)/forgot-password/page-ed263fd46ef81c20.js +1 -0
  91. package/dist/app/_next/static/chunks/app/(public)/layout-f538977545844af8.js +1 -0
  92. package/dist/app/_next/static/chunks/app/(public)/login/page-c0a10b137f346096.js +1 -0
  93. package/dist/app/_next/static/chunks/app/(public)/register/page-4cb7644893efd9b3.js +1 -0
  94. package/dist/app/_next/static/chunks/app/_not-found/page-653f8815b78256cc.js +1 -0
  95. package/dist/app/_next/static/chunks/app/layout-591ca7a3e16528a1.js +1 -0
  96. package/dist/app/_next/static/chunks/app/page-dd19d124b5fa2577.js +1 -0
  97. package/dist/app/_next/static/chunks/c37d3baf.c2ff165f5b02c692.js +1 -0
  98. package/dist/app/_next/static/chunks/d0deef33.0379166a4ec23470.js +1 -0
  99. package/dist/app/_next/static/chunks/fd9d1056-54169f07cd680d6c.js +1 -0
  100. package/dist/app/_next/static/chunks/framework-8e0e0f4a6b83a956.js +1 -0
  101. package/dist/app/_next/static/chunks/main-324e91f5a430cddf.js +1 -0
  102. package/dist/app/_next/static/chunks/main-app-55bcae20c77aaf0e.js +1 -0
  103. package/dist/app/_next/static/chunks/pages/_app-3c9ca398d360b709.js +1 -0
  104. package/dist/app/_next/static/chunks/pages/_error-cf5ca766ac8f493f.js +1 -0
  105. package/dist/app/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  106. package/dist/app/_next/static/chunks/webpack-2c306566f7ee1b63.js +1 -0
  107. package/dist/app/_next/static/css/6c4002bae4e236b2.css +3 -0
  108. package/dist/app/_next/static/css/a275cc2b185e04f8.css +1 -0
  109. package/dist/app/_next/static/eCWhKA8XHqmB1zgFcEtN2/_buildManifest.js +1 -0
  110. package/dist/app/_next/static/eCWhKA8XHqmB1zgFcEtN2/_ssgManifest.js +1 -0
  111. package/dist/app/activity-log/all-activity/index.html +1 -0
  112. package/dist/app/activity-log/all-activity/index.txt +14 -0
  113. package/dist/app/activity-log/email-log/index.html +1 -0
  114. package/dist/app/activity-log/email-log/index.txt +14 -0
  115. package/dist/app/activity-log/index.html +1 -0
  116. package/dist/app/activity-log/index.txt +14 -0
  117. package/dist/app/activity-log/notifications/index.html +1 -0
  118. package/dist/app/activity-log/notifications/index.txt +14 -0
  119. package/dist/app/activity-log/sessions/index.html +1 -0
  120. package/dist/app/activity-log/sessions/index.txt +14 -0
  121. package/dist/app/activity-log/workflow-executions/index.html +1 -0
  122. package/dist/app/activity-log/workflow-executions/index.txt +14 -0
  123. package/dist/app/activity-log/workflow-logs/index.html +1 -0
  124. package/dist/app/activity-log/workflow-logs/index.txt +14 -0
  125. package/dist/app/change-password/index.html +1 -0
  126. package/dist/app/change-password/index.txt +14 -0
  127. package/dist/app/dashboard/index.html +1 -0
  128. package/dist/app/dashboard/index.txt +14 -0
  129. package/dist/app/data-browser/index.html +1 -0
  130. package/dist/app/data-browser/index.txt +14 -0
  131. package/dist/app/file-manager/index.html +1 -0
  132. package/dist/app/file-manager/index.txt +14 -0
  133. package/dist/app/forgot-password/index.html +1 -0
  134. package/dist/app/forgot-password/index.txt +13 -0
  135. package/dist/app/index.html +1 -0
  136. package/dist/app/index.txt +9 -0
  137. package/dist/app/login/index.html +1 -0
  138. package/dist/app/login/index.txt +13 -0
  139. package/dist/app/logo-dark.png +0 -0
  140. package/dist/app/logo-icon.svg +81 -0
  141. package/dist/app/logo-light.png +0 -0
  142. package/dist/app/register/index.html +1 -0
  143. package/dist/app/register/index.txt +13 -0
  144. package/dist/app/settings/migrations/index.html +1 -0
  145. package/dist/app/settings/migrations/index.txt +14 -0
  146. package/dist/app/settings/permissions/index.html +1 -0
  147. package/dist/app/settings/permissions/index.txt +14 -0
  148. package/dist/app/settings/project/index.html +1 -0
  149. package/dist/app/settings/project/index.txt +14 -0
  150. package/dist/app/settings/roles/index.html +1 -0
  151. package/dist/app/settings/roles/index.txt +14 -0
  152. package/dist/app/settings/schema/index.html +1 -0
  153. package/dist/app/settings/schema/index.txt +14 -0
  154. package/dist/app/settings/tasks/index.html +1 -0
  155. package/dist/app/settings/tasks/index.txt +14 -0
  156. package/dist/app/settings/templates/edit/index.html +1 -0
  157. package/dist/app/settings/templates/edit/index.txt +14 -0
  158. package/dist/app/settings/templates/index.html +1 -0
  159. package/dist/app/settings/templates/index.txt +14 -0
  160. package/dist/app/settings/tenants/index.html +1 -0
  161. package/dist/app/settings/tenants/index.txt +14 -0
  162. package/dist/app/users/index.html +1 -0
  163. package/dist/app/users/index.txt +14 -0
  164. package/dist/app/users/invites/index.html +1 -0
  165. package/dist/app/users/invites/index.txt +14 -0
  166. package/dist/app/users/list/index.html +1 -0
  167. package/dist/app/users/list/index.txt +14 -0
  168. package/dist/app/users/preferences/index.html +1 -0
  169. package/dist/app/users/preferences/index.txt +14 -0
  170. package/dist/app/users/user-roles/index.html +1 -0
  171. package/dist/app/users/user-roles/index.txt +14 -0
  172. package/dist/app/workflows/detail/index.html +1 -0
  173. package/dist/app/workflows/detail/index.txt +14 -0
  174. package/dist/app/workflows/edit/index.html +1 -0
  175. package/dist/app/workflows/edit/index.txt +14 -0
  176. package/dist/app/workflows/execution/index.html +1 -0
  177. package/dist/app/workflows/execution/index.txt +14 -0
  178. package/dist/app/workflows/index.html +1 -0
  179. package/dist/app/workflows/index.txt +14 -0
  180. package/dist/app.d.ts +36 -0
  181. package/dist/app.d.ts.map +1 -0
  182. package/dist/app.js +546 -0
  183. package/dist/app.js.map +1 -0
  184. package/dist/auth/adapters/baasix-adapter.d.ts +12 -0
  185. package/dist/auth/adapters/baasix-adapter.d.ts.map +1 -0
  186. package/dist/auth/adapters/baasix-adapter.js +318 -0
  187. package/dist/auth/adapters/baasix-adapter.js.map +1 -0
  188. package/dist/auth/adapters/index.d.ts +6 -0
  189. package/dist/auth/adapters/index.d.ts.map +1 -0
  190. package/dist/auth/adapters/index.js +5 -0
  191. package/dist/auth/adapters/index.js.map +1 -0
  192. package/dist/auth/core.d.ts +73 -0
  193. package/dist/auth/core.d.ts.map +1 -0
  194. package/dist/auth/core.js +528 -0
  195. package/dist/auth/core.js.map +1 -0
  196. package/dist/auth/index.d.ts +56 -0
  197. package/dist/auth/index.d.ts.map +1 -0
  198. package/dist/auth/index.js +58 -0
  199. package/dist/auth/index.js.map +1 -0
  200. package/dist/auth/oauth2/index.d.ts +5 -0
  201. package/dist/auth/oauth2/index.d.ts.map +1 -0
  202. package/dist/auth/oauth2/index.js +5 -0
  203. package/dist/auth/oauth2/index.js.map +1 -0
  204. package/dist/auth/oauth2/utils.d.ts +90 -0
  205. package/dist/auth/oauth2/utils.d.ts.map +1 -0
  206. package/dist/auth/oauth2/utils.js +167 -0
  207. package/dist/auth/oauth2/utils.js.map +1 -0
  208. package/dist/auth/providers/apple.d.ts +28 -0
  209. package/dist/auth/providers/apple.d.ts.map +1 -0
  210. package/dist/auth/providers/apple.js +192 -0
  211. package/dist/auth/providers/apple.js.map +1 -0
  212. package/dist/auth/providers/credential.d.ts +87 -0
  213. package/dist/auth/providers/credential.d.ts.map +1 -0
  214. package/dist/auth/providers/credential.js +162 -0
  215. package/dist/auth/providers/credential.js.map +1 -0
  216. package/dist/auth/providers/facebook.d.ts +26 -0
  217. package/dist/auth/providers/facebook.d.ts.map +1 -0
  218. package/dist/auth/providers/facebook.js +112 -0
  219. package/dist/auth/providers/facebook.js.map +1 -0
  220. package/dist/auth/providers/github.d.ts +29 -0
  221. package/dist/auth/providers/github.d.ts.map +1 -0
  222. package/dist/auth/providers/github.js +144 -0
  223. package/dist/auth/providers/github.js.map +1 -0
  224. package/dist/auth/providers/google.d.ts +32 -0
  225. package/dist/auth/providers/google.d.ts.map +1 -0
  226. package/dist/auth/providers/google.js +145 -0
  227. package/dist/auth/providers/google.js.map +1 -0
  228. package/dist/auth/providers/index.d.ts +22 -0
  229. package/dist/auth/providers/index.d.ts.map +1 -0
  230. package/dist/auth/providers/index.js +17 -0
  231. package/dist/auth/providers/index.js.map +1 -0
  232. package/dist/auth/routes.d.ts +63 -0
  233. package/dist/auth/routes.d.ts.map +1 -0
  234. package/dist/auth/routes.js +827 -0
  235. package/dist/auth/routes.js.map +1 -0
  236. package/dist/auth/services/index.d.ts +10 -0
  237. package/dist/auth/services/index.d.ts.map +1 -0
  238. package/dist/auth/services/index.js +7 -0
  239. package/dist/auth/services/index.js.map +1 -0
  240. package/dist/auth/services/session.d.ts +81 -0
  241. package/dist/auth/services/session.d.ts.map +1 -0
  242. package/dist/auth/services/session.js +186 -0
  243. package/dist/auth/services/session.js.map +1 -0
  244. package/dist/auth/services/token.d.ts +41 -0
  245. package/dist/auth/services/token.d.ts.map +1 -0
  246. package/dist/auth/services/token.js +44 -0
  247. package/dist/auth/services/token.js.map +1 -0
  248. package/dist/auth/services/verification.d.ts +77 -0
  249. package/dist/auth/services/verification.d.ts.map +1 -0
  250. package/dist/auth/services/verification.js +143 -0
  251. package/dist/auth/services/verification.js.map +1 -0
  252. package/dist/auth/types.d.ts +318 -0
  253. package/dist/auth/types.d.ts.map +1 -0
  254. package/dist/auth/types.js +6 -0
  255. package/dist/auth/types.js.map +1 -0
  256. package/dist/customTypes/arrays.d.ts +200 -0
  257. package/dist/customTypes/arrays.d.ts.map +1 -0
  258. package/dist/customTypes/arrays.js +309 -0
  259. package/dist/customTypes/arrays.js.map +1 -0
  260. package/dist/customTypes/index.d.ts +8 -0
  261. package/dist/customTypes/index.d.ts.map +1 -0
  262. package/dist/customTypes/index.js +11 -0
  263. package/dist/customTypes/index.js.map +1 -0
  264. package/dist/customTypes/postgis.d.ts +146 -0
  265. package/dist/customTypes/postgis.d.ts.map +1 -0
  266. package/dist/customTypes/postgis.js +315 -0
  267. package/dist/customTypes/postgis.js.map +1 -0
  268. package/dist/customTypes/ranges.d.ts +128 -0
  269. package/dist/customTypes/ranges.d.ts.map +1 -0
  270. package/dist/customTypes/ranges.js +257 -0
  271. package/dist/customTypes/ranges.js.map +1 -0
  272. package/dist/index.d.ts +37 -0
  273. package/dist/index.d.ts.map +1 -0
  274. package/dist/index.js +42 -0
  275. package/dist/index.js.map +1 -0
  276. package/dist/migrations/0.1.0-alpha.0_initial_setup.d.ts +29 -0
  277. package/dist/migrations/0.1.0-alpha.0_initial_setup.d.ts.map +1 -0
  278. package/dist/migrations/0.1.0-alpha.0_initial_setup.js +72 -0
  279. package/dist/migrations/0.1.0-alpha.0_initial_setup.js.map +1 -0
  280. package/dist/migrations/_example_migration.d.ts +31 -0
  281. package/dist/migrations/_example_migration.d.ts.map +1 -0
  282. package/dist/migrations/_example_migration.js +75 -0
  283. package/dist/migrations/_example_migration.js.map +1 -0
  284. package/dist/plugins/definePlugin.d.ts +49 -0
  285. package/dist/plugins/definePlugin.d.ts.map +1 -0
  286. package/dist/plugins/definePlugin.js +131 -0
  287. package/dist/plugins/definePlugin.js.map +1 -0
  288. package/dist/plugins/softDelete.d.ts +179 -0
  289. package/dist/plugins/softDelete.d.ts.map +1 -0
  290. package/dist/plugins/softDelete.js +235 -0
  291. package/dist/plugins/softDelete.js.map +1 -0
  292. package/dist/routes/auth.route.d.ts +14 -0
  293. package/dist/routes/auth.route.d.ts.map +1 -0
  294. package/dist/routes/auth.route.js +421 -0
  295. package/dist/routes/auth.route.js.map +1 -0
  296. package/dist/routes/file.route.d.ts +7 -0
  297. package/dist/routes/file.route.d.ts.map +1 -0
  298. package/dist/routes/file.route.js +274 -0
  299. package/dist/routes/file.route.js.map +1 -0
  300. package/dist/routes/items.route.d.ts +7 -0
  301. package/dist/routes/items.route.d.ts.map +1 -0
  302. package/dist/routes/items.route.js +369 -0
  303. package/dist/routes/items.route.js.map +1 -0
  304. package/dist/routes/migration.route.d.ts +7 -0
  305. package/dist/routes/migration.route.d.ts.map +1 -0
  306. package/dist/routes/migration.route.js +225 -0
  307. package/dist/routes/migration.route.js.map +1 -0
  308. package/dist/routes/notification.route.d.ts +7 -0
  309. package/dist/routes/notification.route.d.ts.map +1 -0
  310. package/dist/routes/notification.route.js +124 -0
  311. package/dist/routes/notification.route.js.map +1 -0
  312. package/dist/routes/openapi.route.d.ts +7 -0
  313. package/dist/routes/openapi.route.d.ts.map +1 -0
  314. package/dist/routes/openapi.route.js +2169 -0
  315. package/dist/routes/openapi.route.js.map +1 -0
  316. package/dist/routes/permission.route.d.ts +7 -0
  317. package/dist/routes/permission.route.d.ts.map +1 -0
  318. package/dist/routes/permission.route.js +158 -0
  319. package/dist/routes/permission.route.js.map +1 -0
  320. package/dist/routes/realtime.route.d.ts +21 -0
  321. package/dist/routes/realtime.route.d.ts.map +1 -0
  322. package/dist/routes/realtime.route.js +243 -0
  323. package/dist/routes/realtime.route.js.map +1 -0
  324. package/dist/routes/reports.route.d.ts +7 -0
  325. package/dist/routes/reports.route.d.ts.map +1 -0
  326. package/dist/routes/reports.route.js +95 -0
  327. package/dist/routes/reports.route.js.map +1 -0
  328. package/dist/routes/schema.route.d.ts +7 -0
  329. package/dist/routes/schema.route.d.ts.map +1 -0
  330. package/dist/routes/schema.route.js +1780 -0
  331. package/dist/routes/schema.route.js.map +1 -0
  332. package/dist/routes/settings.route.d.ts +7 -0
  333. package/dist/routes/settings.route.d.ts.map +1 -0
  334. package/dist/routes/settings.route.js +154 -0
  335. package/dist/routes/settings.route.js.map +1 -0
  336. package/dist/routes/templates.route.d.ts +7 -0
  337. package/dist/routes/templates.route.d.ts.map +1 -0
  338. package/dist/routes/templates.route.js +91 -0
  339. package/dist/routes/templates.route.js.map +1 -0
  340. package/dist/routes/utils.route.d.ts +7 -0
  341. package/dist/routes/utils.route.d.ts.map +1 -0
  342. package/dist/routes/utils.route.js +33 -0
  343. package/dist/routes/utils.route.js.map +1 -0
  344. package/dist/routes/workflow.route.d.ts +7 -0
  345. package/dist/routes/workflow.route.d.ts.map +1 -0
  346. package/dist/routes/workflow.route.js +787 -0
  347. package/dist/routes/workflow.route.js.map +1 -0
  348. package/dist/services/AssetsService.d.ts +39 -0
  349. package/dist/services/AssetsService.d.ts.map +1 -0
  350. package/dist/services/AssetsService.js +255 -0
  351. package/dist/services/AssetsService.js.map +1 -0
  352. package/dist/services/CacheService.d.ts +169 -0
  353. package/dist/services/CacheService.d.ts.map +1 -0
  354. package/dist/services/CacheService.js +722 -0
  355. package/dist/services/CacheService.js.map +1 -0
  356. package/dist/services/FilesService.d.ts +30 -0
  357. package/dist/services/FilesService.d.ts.map +1 -0
  358. package/dist/services/FilesService.js +268 -0
  359. package/dist/services/FilesService.js.map +1 -0
  360. package/dist/services/HooksManager.d.ts +38 -0
  361. package/dist/services/HooksManager.d.ts.map +1 -0
  362. package/dist/services/HooksManager.js +165 -0
  363. package/dist/services/HooksManager.js.map +1 -0
  364. package/dist/services/ItemsService.d.ts +273 -0
  365. package/dist/services/ItemsService.d.ts.map +1 -0
  366. package/dist/services/ItemsService.js +2458 -0
  367. package/dist/services/ItemsService.js.map +1 -0
  368. package/dist/services/MailService.d.ts +76 -0
  369. package/dist/services/MailService.d.ts.map +1 -0
  370. package/dist/services/MailService.js +585 -0
  371. package/dist/services/MailService.js.map +1 -0
  372. package/dist/services/MigrationService.d.ts +243 -0
  373. package/dist/services/MigrationService.d.ts.map +1 -0
  374. package/dist/services/MigrationService.js +914 -0
  375. package/dist/services/MigrationService.js.map +1 -0
  376. package/dist/services/NotificationService.d.ts +35 -0
  377. package/dist/services/NotificationService.d.ts.map +1 -0
  378. package/dist/services/NotificationService.js +159 -0
  379. package/dist/services/NotificationService.js.map +1 -0
  380. package/dist/services/PermissionService.d.ts +128 -0
  381. package/dist/services/PermissionService.d.ts.map +1 -0
  382. package/dist/services/PermissionService.js +373 -0
  383. package/dist/services/PermissionService.js.map +1 -0
  384. package/dist/services/PluginManager.d.ts +138 -0
  385. package/dist/services/PluginManager.d.ts.map +1 -0
  386. package/dist/services/PluginManager.js +463 -0
  387. package/dist/services/PluginManager.js.map +1 -0
  388. package/dist/services/RealtimeService.d.ts +209 -0
  389. package/dist/services/RealtimeService.d.ts.map +1 -0
  390. package/dist/services/RealtimeService.js +978 -0
  391. package/dist/services/RealtimeService.js.map +1 -0
  392. package/dist/services/ReportService.d.ts +13 -0
  393. package/dist/services/ReportService.d.ts.map +1 -0
  394. package/dist/services/ReportService.js +91 -0
  395. package/dist/services/ReportService.js.map +1 -0
  396. package/dist/services/SettingsService.d.ts +60 -0
  397. package/dist/services/SettingsService.d.ts.map +1 -0
  398. package/dist/services/SettingsService.js +474 -0
  399. package/dist/services/SettingsService.js.map +1 -0
  400. package/dist/services/SocketService.d.ts +129 -0
  401. package/dist/services/SocketService.d.ts.map +1 -0
  402. package/dist/services/SocketService.js +600 -0
  403. package/dist/services/SocketService.js.map +1 -0
  404. package/dist/services/StatsService.d.ts +10 -0
  405. package/dist/services/StatsService.d.ts.map +1 -0
  406. package/dist/services/StatsService.js +40 -0
  407. package/dist/services/StatsService.js.map +1 -0
  408. package/dist/services/StorageService.d.ts +20 -0
  409. package/dist/services/StorageService.d.ts.map +1 -0
  410. package/dist/services/StorageService.js +164 -0
  411. package/dist/services/StorageService.js.map +1 -0
  412. package/dist/services/TasksService.d.ts +74 -0
  413. package/dist/services/TasksService.d.ts.map +1 -0
  414. package/dist/services/TasksService.js +404 -0
  415. package/dist/services/TasksService.js.map +1 -0
  416. package/dist/services/WorkflowService.d.ts +305 -0
  417. package/dist/services/WorkflowService.d.ts.map +1 -0
  418. package/dist/services/WorkflowService.js +1811 -0
  419. package/dist/services/WorkflowService.js.map +1 -0
  420. package/dist/templates/logo/logo.png +0 -0
  421. package/dist/templates/mails/default.liquid +23 -0
  422. package/dist/types/aggregation.d.ts +40 -0
  423. package/dist/types/aggregation.d.ts.map +1 -0
  424. package/dist/types/aggregation.js +6 -0
  425. package/dist/types/aggregation.js.map +1 -0
  426. package/dist/types/assets.d.ts +32 -0
  427. package/dist/types/assets.d.ts.map +1 -0
  428. package/dist/types/assets.js +6 -0
  429. package/dist/types/assets.js.map +1 -0
  430. package/dist/types/auth.d.ts +50 -0
  431. package/dist/types/auth.d.ts.map +1 -0
  432. package/dist/types/auth.js +6 -0
  433. package/dist/types/auth.js.map +1 -0
  434. package/dist/types/cache.d.ts +47 -0
  435. package/dist/types/cache.d.ts.map +1 -0
  436. package/dist/types/cache.js +6 -0
  437. package/dist/types/cache.js.map +1 -0
  438. package/dist/types/database.d.ts +16 -0
  439. package/dist/types/database.d.ts.map +1 -0
  440. package/dist/types/database.js +6 -0
  441. package/dist/types/database.js.map +1 -0
  442. package/dist/types/fields.d.ts +71 -0
  443. package/dist/types/fields.d.ts.map +1 -0
  444. package/dist/types/fields.js +6 -0
  445. package/dist/types/fields.js.map +1 -0
  446. package/dist/types/files.d.ts +33 -0
  447. package/dist/types/files.d.ts.map +1 -0
  448. package/dist/types/files.js +6 -0
  449. package/dist/types/files.js.map +1 -0
  450. package/dist/types/hooks.d.ts +29 -0
  451. package/dist/types/hooks.d.ts.map +1 -0
  452. package/dist/types/hooks.js +6 -0
  453. package/dist/types/hooks.js.map +1 -0
  454. package/dist/types/import-export.d.ts +62 -0
  455. package/dist/types/import-export.d.ts.map +1 -0
  456. package/dist/types/import-export.js +6 -0
  457. package/dist/types/import-export.js.map +1 -0
  458. package/dist/types/index.d.ts +31 -0
  459. package/dist/types/index.d.ts.map +1 -0
  460. package/dist/types/index.js +58 -0
  461. package/dist/types/index.js.map +1 -0
  462. package/dist/types/mail.d.ts +34 -0
  463. package/dist/types/mail.d.ts.map +1 -0
  464. package/dist/types/mail.js +6 -0
  465. package/dist/types/mail.js.map +1 -0
  466. package/dist/types/notifications.d.ts +16 -0
  467. package/dist/types/notifications.d.ts.map +1 -0
  468. package/dist/types/notifications.js +6 -0
  469. package/dist/types/notifications.js.map +1 -0
  470. package/dist/types/plugin.d.ts +351 -0
  471. package/dist/types/plugin.d.ts.map +1 -0
  472. package/dist/types/plugin.js +8 -0
  473. package/dist/types/plugin.js.map +1 -0
  474. package/dist/types/query.d.ts +71 -0
  475. package/dist/types/query.d.ts.map +1 -0
  476. package/dist/types/query.js +6 -0
  477. package/dist/types/query.js.map +1 -0
  478. package/dist/types/relations.d.ts +111 -0
  479. package/dist/types/relations.d.ts.map +1 -0
  480. package/dist/types/relations.js +6 -0
  481. package/dist/types/relations.js.map +1 -0
  482. package/dist/types/reports.d.ts +17 -0
  483. package/dist/types/reports.d.ts.map +1 -0
  484. package/dist/types/reports.js +6 -0
  485. package/dist/types/reports.js.map +1 -0
  486. package/dist/types/schema.d.ts +26 -0
  487. package/dist/types/schema.d.ts.map +1 -0
  488. package/dist/types/schema.js +6 -0
  489. package/dist/types/schema.js.map +1 -0
  490. package/dist/types/seed.d.ts +27 -0
  491. package/dist/types/seed.d.ts.map +1 -0
  492. package/dist/types/seed.js +6 -0
  493. package/dist/types/seed.js.map +1 -0
  494. package/dist/types/services.d.ts +68 -0
  495. package/dist/types/services.d.ts.map +1 -0
  496. package/dist/types/services.js +6 -0
  497. package/dist/types/services.js.map +1 -0
  498. package/dist/types/settings.d.ts +36 -0
  499. package/dist/types/settings.d.ts.map +1 -0
  500. package/dist/types/settings.js +6 -0
  501. package/dist/types/settings.js.map +1 -0
  502. package/dist/types/sockets.d.ts +26 -0
  503. package/dist/types/sockets.d.ts.map +1 -0
  504. package/dist/types/sockets.js +6 -0
  505. package/dist/types/sockets.js.map +1 -0
  506. package/dist/types/sort.d.ts +25 -0
  507. package/dist/types/sort.d.ts.map +1 -0
  508. package/dist/types/sort.js +6 -0
  509. package/dist/types/sort.js.map +1 -0
  510. package/dist/types/spatial.d.ts +19 -0
  511. package/dist/types/spatial.d.ts.map +1 -0
  512. package/dist/types/spatial.js +6 -0
  513. package/dist/types/spatial.js.map +1 -0
  514. package/dist/types/stats.d.ts +21 -0
  515. package/dist/types/stats.d.ts.map +1 -0
  516. package/dist/types/stats.js +6 -0
  517. package/dist/types/stats.js.map +1 -0
  518. package/dist/types/storage.d.ts +19 -0
  519. package/dist/types/storage.d.ts.map +1 -0
  520. package/dist/types/storage.js +6 -0
  521. package/dist/types/storage.js.map +1 -0
  522. package/dist/types/tasks.d.ts +14 -0
  523. package/dist/types/tasks.d.ts.map +1 -0
  524. package/dist/types/tasks.js +6 -0
  525. package/dist/types/tasks.js.map +1 -0
  526. package/dist/types/utils.d.ts +54 -0
  527. package/dist/types/utils.d.ts.map +1 -0
  528. package/dist/types/utils.js +6 -0
  529. package/dist/types/utils.js.map +1 -0
  530. package/dist/types/workflow.d.ts +17 -0
  531. package/dist/types/workflow.d.ts.map +1 -0
  532. package/dist/types/workflow.js +6 -0
  533. package/dist/types/workflow.js.map +1 -0
  534. package/dist/utils/aggregationUtils.d.ts +192 -0
  535. package/dist/utils/aggregationUtils.d.ts.map +1 -0
  536. package/dist/utils/aggregationUtils.js +450 -0
  537. package/dist/utils/aggregationUtils.js.map +1 -0
  538. package/dist/utils/auth.d.ts +93 -0
  539. package/dist/utils/auth.d.ts.map +1 -0
  540. package/dist/utils/auth.js +557 -0
  541. package/dist/utils/auth.js.map +1 -0
  542. package/dist/utils/cache.d.ts +64 -0
  543. package/dist/utils/cache.d.ts.map +1 -0
  544. package/dist/utils/cache.js +464 -0
  545. package/dist/utils/cache.js.map +1 -0
  546. package/dist/utils/common.d.ts +53 -0
  547. package/dist/utils/common.d.ts.map +1 -0
  548. package/dist/utils/common.js +162 -0
  549. package/dist/utils/common.js.map +1 -0
  550. package/dist/utils/db.d.ts +101 -0
  551. package/dist/utils/db.d.ts.map +1 -0
  552. package/dist/utils/db.js +413 -0
  553. package/dist/utils/db.js.map +1 -0
  554. package/dist/utils/dirname.d.ts +30 -0
  555. package/dist/utils/dirname.d.ts.map +1 -0
  556. package/dist/utils/dirname.js +95 -0
  557. package/dist/utils/dirname.js.map +1 -0
  558. package/dist/utils/dynamicVariableResolver.d.ts +17 -0
  559. package/dist/utils/dynamicVariableResolver.d.ts.map +1 -0
  560. package/dist/utils/dynamicVariableResolver.js +262 -0
  561. package/dist/utils/dynamicVariableResolver.js.map +1 -0
  562. package/dist/utils/env.d.ts +38 -0
  563. package/dist/utils/env.d.ts.map +1 -0
  564. package/dist/utils/env.js +80 -0
  565. package/dist/utils/env.js.map +1 -0
  566. package/dist/utils/errorHandler.d.ts +14 -0
  567. package/dist/utils/errorHandler.d.ts.map +1 -0
  568. package/dist/utils/errorHandler.js +79 -0
  569. package/dist/utils/errorHandler.js.map +1 -0
  570. package/dist/utils/fieldExpansion.d.ts +30 -0
  571. package/dist/utils/fieldExpansion.d.ts.map +1 -0
  572. package/dist/utils/fieldExpansion.js +145 -0
  573. package/dist/utils/fieldExpansion.js.map +1 -0
  574. package/dist/utils/fieldUtils.d.ts +179 -0
  575. package/dist/utils/fieldUtils.d.ts.map +1 -0
  576. package/dist/utils/fieldUtils.js +424 -0
  577. package/dist/utils/fieldUtils.js.map +1 -0
  578. package/dist/utils/filterOperators.d.ts +472 -0
  579. package/dist/utils/filterOperators.d.ts.map +1 -0
  580. package/dist/utils/filterOperators.js +1229 -0
  581. package/dist/utils/filterOperators.js.map +1 -0
  582. package/dist/utils/importUtils.d.ts +127 -0
  583. package/dist/utils/importUtils.d.ts.map +1 -0
  584. package/dist/utils/importUtils.js +437 -0
  585. package/dist/utils/importUtils.js.map +1 -0
  586. package/dist/utils/index.d.ts +75 -0
  587. package/dist/utils/index.d.ts.map +1 -0
  588. package/dist/utils/index.js +101 -0
  589. package/dist/utils/index.js.map +1 -0
  590. package/dist/utils/logger.d.ts +41 -0
  591. package/dist/utils/logger.d.ts.map +1 -0
  592. package/dist/utils/logger.js +217 -0
  593. package/dist/utils/logger.js.map +1 -0
  594. package/dist/utils/orderUtils.d.ts +117 -0
  595. package/dist/utils/orderUtils.d.ts.map +1 -0
  596. package/dist/utils/orderUtils.js +249 -0
  597. package/dist/utils/orderUtils.js.map +1 -0
  598. package/dist/utils/queryBuilder.d.ts +118 -0
  599. package/dist/utils/queryBuilder.d.ts.map +1 -0
  600. package/dist/utils/queryBuilder.js +489 -0
  601. package/dist/utils/queryBuilder.js.map +1 -0
  602. package/dist/utils/relationLoader.d.ts +65 -0
  603. package/dist/utils/relationLoader.d.ts.map +1 -0
  604. package/dist/utils/relationLoader.js +1081 -0
  605. package/dist/utils/relationLoader.js.map +1 -0
  606. package/dist/utils/relationPathResolver.d.ts +30 -0
  607. package/dist/utils/relationPathResolver.d.ts.map +1 -0
  608. package/dist/utils/relationPathResolver.js +173 -0
  609. package/dist/utils/relationPathResolver.js.map +1 -0
  610. package/dist/utils/relationUtils.d.ts +139 -0
  611. package/dist/utils/relationUtils.d.ts.map +1 -0
  612. package/dist/utils/relationUtils.js +711 -0
  613. package/dist/utils/relationUtils.js.map +1 -0
  614. package/dist/utils/router.d.ts +6 -0
  615. package/dist/utils/router.d.ts.map +1 -0
  616. package/dist/utils/router.js +95 -0
  617. package/dist/utils/router.js.map +1 -0
  618. package/dist/utils/schema.d.ts +88 -0
  619. package/dist/utils/schema.d.ts.map +1 -0
  620. package/dist/utils/schema.js +24 -0
  621. package/dist/utils/schema.js.map +1 -0
  622. package/dist/utils/schemaManager.d.ts +238 -0
  623. package/dist/utils/schemaManager.d.ts.map +1 -0
  624. package/dist/utils/schemaManager.js +1992 -0
  625. package/dist/utils/schemaManager.js.map +1 -0
  626. package/dist/utils/schemaValidator.d.ts +83 -0
  627. package/dist/utils/schemaValidator.d.ts.map +1 -0
  628. package/dist/utils/schemaValidator.js +491 -0
  629. package/dist/utils/schemaValidator.js.map +1 -0
  630. package/dist/utils/seed.d.ts +45 -0
  631. package/dist/utils/seed.d.ts.map +1 -0
  632. package/dist/utils/seed.js +248 -0
  633. package/dist/utils/seed.js.map +1 -0
  634. package/dist/utils/sessionCleanup.d.ts +10 -0
  635. package/dist/utils/sessionCleanup.d.ts.map +1 -0
  636. package/dist/utils/sessionCleanup.js +49 -0
  637. package/dist/utils/sessionCleanup.js.map +1 -0
  638. package/dist/utils/sortUtils.d.ts +117 -0
  639. package/dist/utils/sortUtils.d.ts.map +1 -0
  640. package/dist/utils/sortUtils.js +232 -0
  641. package/dist/utils/sortUtils.js.map +1 -0
  642. package/dist/utils/spatialUtils.d.ts +244 -0
  643. package/dist/utils/spatialUtils.d.ts.map +1 -0
  644. package/dist/utils/spatialUtils.js +359 -0
  645. package/dist/utils/spatialUtils.js.map +1 -0
  646. package/dist/utils/systemschema.d.ts +11040 -0
  647. package/dist/utils/systemschema.d.ts.map +1 -0
  648. package/dist/utils/systemschema.js +1777 -0
  649. package/dist/utils/systemschema.js.map +1 -0
  650. package/dist/utils/tenantUtils.d.ts +34 -0
  651. package/dist/utils/tenantUtils.d.ts.map +1 -0
  652. package/dist/utils/tenantUtils.js +124 -0
  653. package/dist/utils/tenantUtils.js.map +1 -0
  654. package/dist/utils/typeMapper.d.ts +25 -0
  655. package/dist/utils/typeMapper.d.ts.map +1 -0
  656. package/dist/utils/typeMapper.js +282 -0
  657. package/dist/utils/typeMapper.js.map +1 -0
  658. package/dist/utils/valueValidator.d.ts +60 -0
  659. package/dist/utils/valueValidator.d.ts.map +1 -0
  660. package/dist/utils/valueValidator.js +303 -0
  661. package/dist/utils/valueValidator.js.map +1 -0
  662. package/dist/utils/workflow.d.ts +87 -0
  663. package/dist/utils/workflow.d.ts.map +1 -0
  664. package/dist/utils/workflow.js +205 -0
  665. package/dist/utils/workflow.js.map +1 -0
  666. package/package.json +115 -0
@@ -0,0 +1,1992 @@
1
+ import { pgTable, text, jsonb, timestamp } from 'drizzle-orm/pg-core';
2
+ import { eq, inArray } from 'drizzle-orm';
3
+ import argon2 from 'argon2';
4
+ import { getDatabase, getSqlClient, isPgVersionAtLeast } from './db.js';
5
+ import { mapJsonTypeToDrizzle, isRelationField } from './typeMapper.js';
6
+ import { relationBuilder, createForeignKeySQL } from './relationUtils.js';
7
+ import systemSchemaModule from './systemschema.js';
8
+ import env from './env.js';
9
+ const systemSchemas = systemSchemaModule.schemas;
10
+ /**
11
+ * baasix_SchemaDefinition table schema
12
+ * Note: This is duplicated from schema.ts to avoid circular dependency
13
+ */
14
+ const baasixSchemaDefinition = pgTable('baasix_SchemaDefinition', {
15
+ collectionName: text('collectionName').primaryKey().notNull(),
16
+ schema: jsonb('schema').notNull(),
17
+ createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow(),
18
+ updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow(),
19
+ });
20
+ /**
21
+ * Manages dynamic schema generation from JSON definitions
22
+ */
23
+ export class SchemaManager {
24
+ static instance;
25
+ schemas = new Map(); // Stores Drizzle table schemas
26
+ schemaDefinitions = new Map(); // Stores JSON schema definitions
27
+ relations = new Map();
28
+ initialized = false;
29
+ pluginSchemas = []; // Stores plugin schemas
30
+ constructor() { }
31
+ /**
32
+ * Register plugin schemas before initialization
33
+ * This should be called before initialize() to ensure plugin schemas are created with system schemas
34
+ */
35
+ registerPluginSchemas(schemas) {
36
+ if (this.initialized) {
37
+ console.warn('SchemaManager already initialized. Plugin schemas should be registered before initialize().');
38
+ }
39
+ this.pluginSchemas = schemas;
40
+ console.log(`Registered ${schemas.length} plugin schema(s)`);
41
+ }
42
+ /**
43
+ * Get all registered plugin schemas
44
+ */
45
+ getPluginSchemas() {
46
+ return this.pluginSchemas;
47
+ }
48
+ /**
49
+ * Get singleton instance
50
+ */
51
+ static getInstance() {
52
+ // Use globalThis to ensure singleton across different module loading paths
53
+ if (!globalThis.__baasix_schemaManager) {
54
+ globalThis.__baasix_schemaManager = new SchemaManager();
55
+ }
56
+ return globalThis.__baasix_schemaManager;
57
+ }
58
+ /**
59
+ * Initialize schema manager by loading all schemas from database
60
+ * Flow matches Sequelize implementation:
61
+ * 1. Ensure SchemaDefinition table exists
62
+ * 2. Ensure system schemas are in SchemaDefinition table
63
+ * 3. Create tables for schemas that need syncing
64
+ * 4. Load all schemas
65
+ * 5. Seed database if empty
66
+ */
67
+ async initialize() {
68
+ if (this.initialized) {
69
+ console.log('SchemaManager already initialized.');
70
+ return;
71
+ }
72
+ console.log('Initializing Schema Manager...');
73
+ try {
74
+ // Step 0: Enable required PostgreSQL extensions
75
+ await this.enablePostgresExtensions();
76
+ // Step 1: Ensure baasix_SchemaDefinition table exists
77
+ await this.ensureSchemaDefinitionTable();
78
+ // Step 2: Ensure system schemas are in the table
79
+ const needSyncing = await this.ensureSystemSchemas();
80
+ // Step 3: Create/sync tables for schemas that need it
81
+ if (needSyncing.length > 0) {
82
+ console.info('Need to sync the following schemas:', needSyncing);
83
+ await this.loadAndCreateAllSchemas(needSyncing);
84
+ }
85
+ else {
86
+ console.info('No system schemas need syncing.');
87
+ }
88
+ // Step 4: Load all schemas into memory (pass needSyncing to skip unnecessary sync for unchanged schemas)
89
+ await this.loadAllSchemas(needSyncing);
90
+ this.initialized = true;
91
+ console.log('Schema Manager initialized successfully');
92
+ }
93
+ catch (error) {
94
+ console.error('Failed to initialize Schema Manager:', error);
95
+ throw error;
96
+ }
97
+ }
98
+ /**
99
+ * Enable required PostgreSQL extensions
100
+ */
101
+ async enablePostgresExtensions() {
102
+ const sql = getSqlClient();
103
+ try {
104
+ // Enable pgcrypto for gen_random_uuid()
105
+ await sql.unsafe('CREATE EXTENSION IF NOT EXISTS pgcrypto');
106
+ console.log('PostgreSQL extension pgcrypto enabled');
107
+ // Enable PostGIS if configured
108
+ if (env.get('DATABASE_POSTGIS') === 'true') {
109
+ await sql.unsafe('CREATE EXTENSION IF NOT EXISTS postgis');
110
+ console.log('PostgreSQL extension postgis enabled');
111
+ }
112
+ }
113
+ catch (error) {
114
+ console.error('Failed to enable PostgreSQL extensions:', error);
115
+ throw error;
116
+ }
117
+ }
118
+ /**
119
+ * Ensure baasix_SchemaDefinition table exists
120
+ */
121
+ async ensureSchemaDefinitionTable() {
122
+ const sql = getSqlClient();
123
+ // Check if table exists
124
+ const result = await sql `
125
+ SELECT EXISTS (
126
+ SELECT FROM information_schema.tables
127
+ WHERE table_name = 'baasix_SchemaDefinition'
128
+ )
129
+ `;
130
+ if (!result[0].exists) {
131
+ console.log('Creating baasix_SchemaDefinition table...');
132
+ await sql `
133
+ CREATE TABLE "baasix_SchemaDefinition" (
134
+ "collectionName" TEXT PRIMARY KEY NOT NULL,
135
+ schema JSONB NOT NULL,
136
+ "createdAt" TIMESTAMPTZ DEFAULT NOW(),
137
+ "updatedAt" TIMESTAMPTZ DEFAULT NOW()
138
+ )
139
+ `;
140
+ console.log('SchemaDefinition table created.');
141
+ }
142
+ }
143
+ /**
144
+ * Ensure system schemas and plugin schemas are in the baasix_SchemaDefinition table
145
+ * Returns list of schemas that need syncing
146
+ */
147
+ async ensureSystemSchemas() {
148
+ const db = getDatabase();
149
+ const needUpdate = [];
150
+ // Combine system schemas with plugin schemas
151
+ const allSchemas = [
152
+ ...systemSchemas,
153
+ ...this.pluginSchemas.map(ps => ({
154
+ collectionName: ps.collectionName,
155
+ schema: ps.schema
156
+ }))
157
+ ];
158
+ for (const schemaData of allSchemas) {
159
+ // Prepare schema with timestamp fields added if timestamps: true
160
+ const schemaToStore = JSON.parse(JSON.stringify(schemaData.schema));
161
+ if (schemaToStore.timestamps !== false) {
162
+ // Add createdAt and updatedAt fields to schema definition if not already present
163
+ if (!schemaToStore.fields.createdAt) {
164
+ schemaToStore.fields.createdAt = {
165
+ type: "DateTime",
166
+ allowNull: true,
167
+ SystemGenerated: "true",
168
+ defaultValue: { type: "NOW" }
169
+ };
170
+ }
171
+ if (!schemaToStore.fields.updatedAt) {
172
+ schemaToStore.fields.updatedAt = {
173
+ type: "DateTime",
174
+ allowNull: true,
175
+ SystemGenerated: "true",
176
+ defaultValue: { type: "NOW" }
177
+ };
178
+ }
179
+ }
180
+ // Add deletedAt if paranoid mode
181
+ if (schemaToStore.paranoid && !schemaToStore.fields.deletedAt) {
182
+ schemaToStore.fields.deletedAt = {
183
+ type: "DateTime",
184
+ allowNull: true,
185
+ SystemGenerated: "true"
186
+ };
187
+ }
188
+ // Check if schema already exists
189
+ const existing = await db
190
+ .select()
191
+ .from(baasixSchemaDefinition)
192
+ .where(eq(baasixSchemaDefinition.collectionName, schemaData.collectionName))
193
+ .limit(1);
194
+ if (existing.length === 0) {
195
+ // Insert new schema
196
+ await db.insert(baasixSchemaDefinition).values({
197
+ collectionName: schemaData.collectionName,
198
+ schema: schemaToStore,
199
+ });
200
+ console.log(`Added system schema: ${schemaData.collectionName}`);
201
+ needUpdate.push(schemaData.collectionName);
202
+ }
203
+ else {
204
+ // Compare and update if needed (add new fields, check modified fields, and sync schema-level properties)
205
+ const existingSchema = existing[0].schema;
206
+ let hasChanges = false;
207
+ // Ensure existingSchema.fields exists
208
+ if (!existingSchema.fields) {
209
+ existingSchema.fields = {};
210
+ }
211
+ // Helper function to normalize schema for comparison
212
+ // Removes SystemGenerated field which can differ between code ("true") and db (true)
213
+ const normalizeForComparison = (obj) => {
214
+ if (obj === null || typeof obj !== 'object')
215
+ return obj;
216
+ if (Array.isArray(obj)) {
217
+ return obj.map(normalizeForComparison);
218
+ }
219
+ const result = {};
220
+ for (const [key, value] of Object.entries(obj)) {
221
+ if (key === 'SystemGenerated')
222
+ continue; // Skip this field for comparison
223
+ result[key] = normalizeForComparison(value);
224
+ }
225
+ return result;
226
+ };
227
+ // Helper function for deep equality comparison (ignores property order)
228
+ const deepEqual = (a, b) => {
229
+ if (a === b)
230
+ return true;
231
+ if (a == null || b == null)
232
+ return a === b;
233
+ if (typeof a !== typeof b)
234
+ return false;
235
+ if (typeof a !== 'object')
236
+ return a === b;
237
+ if (Array.isArray(a) !== Array.isArray(b))
238
+ return false;
239
+ if (Array.isArray(a)) {
240
+ if (a.length !== b.length)
241
+ return false;
242
+ // For arrays, compare each element
243
+ // Sort arrays of objects by 'name' field for stable comparison
244
+ const sortKey = (item) => item?.name || JSON.stringify(item);
245
+ const sortedA = [...a].sort((x, y) => String(sortKey(x)).localeCompare(String(sortKey(y))));
246
+ const sortedB = [...b].sort((x, y) => String(sortKey(x)).localeCompare(String(sortKey(y))));
247
+ return sortedA.every((item, i) => deepEqual(item, sortedB[i]));
248
+ }
249
+ // For objects, compare all keys regardless of order
250
+ const keysA = Object.keys(a);
251
+ const keysB = Object.keys(b);
252
+ if (keysA.length !== keysB.length)
253
+ return false;
254
+ return keysA.every(key => deepEqual(a[key], b[key]));
255
+ };
256
+ // Check for new fields (fields in system schema but not in DB)
257
+ const newFields = Object.keys(schemaToStore.fields || {}).filter((field) => !existingSchema.fields[field]);
258
+ if (newFields.length > 0) {
259
+ for (const field of newFields) {
260
+ existingSchema.fields[field] = schemaToStore.fields[field];
261
+ }
262
+ hasChanges = true;
263
+ console.log(`[SCHEMA DIFF] ${schemaData.collectionName} new fields:`, newFields);
264
+ }
265
+ // Check for modified fields (existing fields with changed definitions)
266
+ const modifiedFields = [];
267
+ for (const fieldName of Object.keys(schemaToStore.fields || {})) {
268
+ if (existingSchema.fields[fieldName]) {
269
+ // Field exists in both - check if definition changed (normalize to ignore SystemGenerated differences)
270
+ const normalizedExisting = normalizeForComparison(existingSchema.fields[fieldName]);
271
+ const normalizedNew = normalizeForComparison(schemaToStore.fields[fieldName]);
272
+ if (!deepEqual(normalizedExisting, normalizedNew)) {
273
+ existingSchema.fields[fieldName] = schemaToStore.fields[fieldName];
274
+ modifiedFields.push(fieldName);
275
+ hasChanges = true;
276
+ }
277
+ }
278
+ }
279
+ if (modifiedFields.length > 0) {
280
+ console.log(`[SCHEMA DIFF] ${schemaData.collectionName} modified fields:`, modifiedFields);
281
+ }
282
+ // Note: We intentionally do NOT remove fields that exist in DB but not in system schema
283
+ // This preserves user data and allows for gradual migrations
284
+ // Sync structural schema-level properties (indexes, name) from system schema
285
+ // These are enforced from code and should not be user-modified
286
+ const structuralProps = ['indexes', 'name'];
287
+ for (const prop of structuralProps) {
288
+ // Normalize both values before comparison to ignore SystemGenerated differences
289
+ const normalizedExisting = normalizeForComparison(existingSchema[prop]);
290
+ const normalizedNew = normalizeForComparison(schemaToStore[prop]);
291
+ if (schemaToStore[prop] !== undefined && !deepEqual(normalizedExisting, normalizedNew)) {
292
+ console.log(`[SCHEMA DIFF] ${schemaData.collectionName} property '${prop}' differs:`, {
293
+ existing: existingSchema[prop],
294
+ new: schemaToStore[prop]
295
+ });
296
+ existingSchema[prop] = schemaToStore[prop];
297
+ hasChanges = true;
298
+ }
299
+ }
300
+ // User-configurable properties (timestamps, paranoid, usertrack, sortEnabled)
301
+ // Only set these if they don't exist in the DB yet - preserve user changes
302
+ const userConfigurableProps = ['timestamps', 'paranoid', 'usertrack', 'sortEnabled'];
303
+ for (const prop of userConfigurableProps) {
304
+ // Only set if the property doesn't exist in the existing schema (initial setup)
305
+ if (existingSchema[prop] === undefined && schemaToStore[prop] !== undefined) {
306
+ existingSchema[prop] = schemaToStore[prop];
307
+ hasChanges = true;
308
+ console.log(`Set initial ${prop} for ${schemaData.collectionName}`);
309
+ }
310
+ }
311
+ if (hasChanges) {
312
+ await db
313
+ .update(baasixSchemaDefinition)
314
+ .set({
315
+ schema: existingSchema,
316
+ updatedAt: new Date()
317
+ })
318
+ .where(eq(baasixSchemaDefinition.collectionName, schemaData.collectionName));
319
+ console.log(`Updated system schema: ${schemaData.collectionName}`);
320
+ needUpdate.push(schemaData.collectionName);
321
+ }
322
+ }
323
+ }
324
+ console.log('System schemas ensured in SchemaDefinition table.');
325
+ return needUpdate;
326
+ }
327
+ /**
328
+ * Sort schemas by dependency order (topological sort)
329
+ */
330
+ sortSchemasByDependencies(schemas) {
331
+ // Build dependency graph
332
+ const dependencies = new Map();
333
+ const schemaMap = new Map();
334
+ for (const schemaDef of schemas) {
335
+ const collectionName = schemaDef.collectionName;
336
+ schemaMap.set(collectionName, schemaDef);
337
+ dependencies.set(collectionName, new Set());
338
+ // Find all BelongsTo relations (foreign key dependencies)
339
+ const schema = schemaDef.schema;
340
+ if (schema.fields) {
341
+ for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
342
+ const fs = fieldSchema;
343
+ if (fs.relType === 'BelongsTo' && fs.target && fs.target !== collectionName) {
344
+ dependencies.get(collectionName).add(fs.target);
345
+ }
346
+ }
347
+ }
348
+ }
349
+ // Topological sort using Kahn's algorithm
350
+ const sorted = [];
351
+ const inDegree = new Map();
352
+ const queue = [];
353
+ // Calculate in-degrees (number of dependencies for each table)
354
+ for (const [node, deps] of dependencies) {
355
+ inDegree.set(node, deps.size);
356
+ }
357
+ // Debug: Log dependencies
358
+ console.log('Dependencies map:');
359
+ for (const [node, deps] of dependencies) {
360
+ if (deps.size > 0) {
361
+ console.log(` ${node} depends on: [${Array.from(deps).join(', ')}]`);
362
+ }
363
+ }
364
+ // Find nodes with no dependencies (in-degree = 0)
365
+ for (const [node, degree] of inDegree) {
366
+ if (degree === 0) {
367
+ queue.push(node);
368
+ }
369
+ }
370
+ console.log('Starting with tables that have no dependencies:', queue.join(', '));
371
+ // Process queue
372
+ while (queue.length > 0) {
373
+ const node = queue.shift();
374
+ const schemaDef = schemaMap.get(node);
375
+ if (schemaDef) {
376
+ sorted.push(schemaDef);
377
+ }
378
+ // Reduce in-degree for nodes that depend on this one
379
+ for (const [otherNode, deps] of dependencies) {
380
+ if (deps.has(node)) {
381
+ const newDegree = (inDegree.get(otherNode) || 0) - 1;
382
+ inDegree.set(otherNode, newDegree);
383
+ if (newDegree === 0) {
384
+ queue.push(otherNode);
385
+ }
386
+ }
387
+ }
388
+ }
389
+ // If sorted length != schemas length, there's a circular dependency
390
+ // In that case, just return original order and let FK constraints be added later
391
+ if (sorted.length !== schemas.length) {
392
+ console.info('Circular dependency detected in schemas, using original order');
393
+ return schemas;
394
+ }
395
+ return sorted;
396
+ }
397
+ /**
398
+ * Load and create tables for specific schemas
399
+ */
400
+ async loadAndCreateAllSchemas(needSyncing) {
401
+ const db = getDatabase();
402
+ const schemas = await db
403
+ .select()
404
+ .from(baasixSchemaDefinition)
405
+ .where(inArray(baasixSchemaDefinition.collectionName, needSyncing));
406
+ // Sort schemas by dependency order to ensure referenced tables exist first
407
+ const sortedSchemas = this.sortSchemasByDependencies(schemas);
408
+ console.log('Schema creation order:', sortedSchemas.map(s => s.collectionName).join(', '));
409
+ // First pass: Create all tables and models without FK constraints
410
+ for (const schemaDef of sortedSchemas) {
411
+ await this.createOrUpdateModel(schemaDef.collectionName, schemaDef.schema);
412
+ // Create table using raw SQL (FK constraints will be added in second pass)
413
+ await this.createTableFromSchema(schemaDef.collectionName, schemaDef.schema, true);
414
+ }
415
+ // Second pass: Add foreign key constraints
416
+ console.log('Adding foreign key constraints...');
417
+ for (const schemaDef of sortedSchemas) {
418
+ await this.ensureForeignKeyConstraints(schemaDef.collectionName, schemaDef.schema);
419
+ }
420
+ console.log('All schemas loaded, models created/updated.');
421
+ }
422
+ /**
423
+ * Normalize legacy Sequelize schemas - add default values for timestamp fields
424
+ * and update DB column defaults if missing
425
+ */
426
+ async normalizeLegacySchema(collectionName, schema) {
427
+ const sql = getSqlClient();
428
+ const db = getDatabase();
429
+ let schemaUpdated = false;
430
+ const normalizedSchema = { ...schema, fields: { ...schema.fields } };
431
+ // Check timestamp fields if timestamps are enabled (default: true)
432
+ if (schema.timestamps !== false) {
433
+ // Normalize createdAt field
434
+ if (normalizedSchema.fields.createdAt && !normalizedSchema.fields.createdAt.defaultValue) {
435
+ normalizedSchema.fields.createdAt = {
436
+ ...normalizedSchema.fields.createdAt,
437
+ defaultValue: { type: "NOW" }
438
+ };
439
+ schemaUpdated = true;
440
+ // Also update DB column default
441
+ try {
442
+ await sql.unsafe(`ALTER TABLE "${collectionName}" ALTER COLUMN "createdAt" SET DEFAULT NOW()`);
443
+ console.log(`Updated createdAt default value for ${collectionName}`);
444
+ }
445
+ catch (error) {
446
+ // Column might not exist yet or already have default, ignore
447
+ }
448
+ }
449
+ // Normalize updatedAt field
450
+ if (normalizedSchema.fields.updatedAt && !normalizedSchema.fields.updatedAt.defaultValue) {
451
+ normalizedSchema.fields.updatedAt = {
452
+ ...normalizedSchema.fields.updatedAt,
453
+ defaultValue: { type: "NOW" }
454
+ };
455
+ schemaUpdated = true;
456
+ // Also update DB column default
457
+ try {
458
+ await sql.unsafe(`ALTER TABLE "${collectionName}" ALTER COLUMN "updatedAt" SET DEFAULT NOW()`);
459
+ console.log(`Updated updatedAt default value for ${collectionName}`);
460
+ }
461
+ catch (error) {
462
+ // Column might not exist yet or already have default, ignore
463
+ }
464
+ }
465
+ }
466
+ // Update schema definition in database if changed
467
+ if (schemaUpdated) {
468
+ await db
469
+ .update(baasixSchemaDefinition)
470
+ .set({ schema: normalizedSchema })
471
+ .where(eq(baasixSchemaDefinition.collectionName, collectionName));
472
+ console.log(`Normalized legacy schema definition for ${collectionName}`);
473
+ }
474
+ return normalizedSchema;
475
+ }
476
+ /**
477
+ * Load all schemas from database into memory
478
+ * @param schemasNeedingSync - List of schema names that need table sync (from ensureSystemSchemas)
479
+ * If empty, skip expensive sync operations for faster startup
480
+ */
481
+ async loadAllSchemas(schemasNeedingSync = []) {
482
+ const db = getDatabase();
483
+ const sql = getSqlClient();
484
+ const needsSyncSet = new Set(schemasNeedingSync);
485
+ const skipSync = schemasNeedingSync.length === 0;
486
+ const schemaDefinitions = await db
487
+ .select()
488
+ .from(baasixSchemaDefinition);
489
+ console.log(`Found ${schemaDefinitions.length} schema definitions`);
490
+ // Get list of existing tables in one query (for fast lookup)
491
+ const existingTables = await sql `
492
+ SELECT table_name FROM information_schema.tables
493
+ WHERE table_schema = 'public'
494
+ `;
495
+ const existingTableSet = new Set(existingTables.map((t) => t.table_name));
496
+ if (skipSync) {
497
+ console.log('No schema changes detected, using fast startup path');
498
+ }
499
+ // Sort schemas by dependency order
500
+ const sortedSchemas = this.sortSchemasByDependencies(schemaDefinitions);
501
+ // Schemas that need FK constraint sync (only those that were synced)
502
+ const schemasForFKSync = [];
503
+ // First pass: Create all tables and models without FK constraints
504
+ for (const schemaDef of sortedSchemas) {
505
+ const tableExists = existingTableSet.has(schemaDef.collectionName);
506
+ // Determine if this schema needs table sync:
507
+ // 1. If it was explicitly marked as needing sync
508
+ // 2. If the table doesn't exist yet (new schema)
509
+ // 3. If skipSync is false (there were schema changes, so sync all)
510
+ const needsSync = needsSyncSet.has(schemaDef.collectionName) ||
511
+ !tableExists ||
512
+ !skipSync;
513
+ let normalizedSchema = schemaDef.schema;
514
+ // Only run legacy normalization if sync is needed (skips DB queries on fast path)
515
+ if (needsSync) {
516
+ normalizedSchema = await this.normalizeLegacySchema(schemaDef.collectionName, schemaDef.schema);
517
+ // Update the schemaDef with normalized schema for subsequent operations
518
+ schemaDef.schema = normalizedSchema;
519
+ }
520
+ // Store JSON schema definition for later use (e.g., in getPrimaryKey)
521
+ this.schemaDefinitions.set(schemaDef.collectionName, schemaDef);
522
+ await this.createOrUpdateModel(schemaDef.collectionName, normalizedSchema);
523
+ if (needsSync) {
524
+ // Create table if it doesn't exist (FK constraints will be added in second pass)
525
+ await this.createTableFromSchema(schemaDef.collectionName, normalizedSchema, true);
526
+ schemasForFKSync.push(schemaDef);
527
+ }
528
+ }
529
+ // Second pass: Add foreign key constraints only for schemas that were synced
530
+ if (schemasForFKSync.length > 0) {
531
+ console.log(`Adding foreign key constraints for ${schemasForFKSync.length} schemas...`);
532
+ for (const schemaDef of schemasForFKSync) {
533
+ await this.ensureForeignKeyConstraints(schemaDef.collectionName, schemaDef.schema);
534
+ }
535
+ }
536
+ else {
537
+ console.log('Skipping foreign key constraint check (no schemas need sync)');
538
+ }
539
+ // Check if we need to seed the database
540
+ await this.checkAndSeedDatabase();
541
+ }
542
+ /**
543
+ * Sync table columns with schema definition (add missing columns)
544
+ */
545
+ async syncTableColumns(collectionName, schema) {
546
+ const sql = getSqlClient();
547
+ // Get existing columns in the table
548
+ const existingColumns = await sql `
549
+ SELECT column_name, data_type
550
+ FROM information_schema.columns
551
+ WHERE table_name = ${collectionName}
552
+ `;
553
+ const existingColumnNames = existingColumns.map((col) => col.column_name);
554
+ // Check each field in schema
555
+ for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
556
+ const fs = fieldSchema;
557
+ // Skip relation fields that don't have an explicit type
558
+ if (fs.relType && !fs.type) {
559
+ // For BelongsTo relations, check if the foreign key column needs to be added
560
+ if (fs.relType === 'BelongsTo') {
561
+ const foreignKey = fs.foreignKey || `${fieldName}_Id`;
562
+ if (!existingColumnNames.includes(foreignKey)) {
563
+ // Column doesn't exist, add it via ensureForeignKeyConstraints
564
+ continue;
565
+ }
566
+ }
567
+ continue;
568
+ }
569
+ // Check if column exists
570
+ if (!existingColumnNames.includes(fieldName)) {
571
+ // Column is missing, add it
572
+ const columnDef = this.buildColumnDefinition(fieldName, fs);
573
+ if (columnDef) {
574
+ // Extract just the type and constraints from columnDef (remove field name)
575
+ const columnDefParts = columnDef.split(' ').slice(1).join(' '); // Remove first part which is field name
576
+ try {
577
+ // Use IF NOT EXISTS for safety in case of race conditions or schema query issues
578
+ await sql.unsafe(`ALTER TABLE "${collectionName}" ADD COLUMN IF NOT EXISTS ${columnDef}`);
579
+ console.log(`Added missing column ${fieldName} to ${collectionName}`);
580
+ }
581
+ catch (error) {
582
+ console.error(`Failed to add column ${fieldName} to ${collectionName}:`, error);
583
+ }
584
+ }
585
+ }
586
+ }
587
+ // Add timestamp columns if needed
588
+ if (schema.timestamps !== false) {
589
+ if (!existingColumnNames.includes('createdAt')) {
590
+ try {
591
+ await sql.unsafe(`ALTER TABLE "${collectionName}" ADD COLUMN IF NOT EXISTS "createdAt" TIMESTAMPTZ DEFAULT NOW()`);
592
+ console.log(`Added createdAt column to ${collectionName}`);
593
+ }
594
+ catch (error) {
595
+ // Column might already exist due to race condition, ignore
596
+ }
597
+ }
598
+ if (!existingColumnNames.includes('updatedAt')) {
599
+ try {
600
+ await sql.unsafe(`ALTER TABLE "${collectionName}" ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMPTZ DEFAULT NOW()`);
601
+ console.log(`Added updatedAt column to ${collectionName}`);
602
+ }
603
+ catch (error) {
604
+ // Column might already exist due to race condition, ignore
605
+ }
606
+ }
607
+ }
608
+ // Add deletedAt column if paranoid mode is enabled
609
+ if (schema.paranoid && !existingColumnNames.includes('deletedAt')) {
610
+ try {
611
+ await sql.unsafe(`ALTER TABLE "${collectionName}" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMPTZ`);
612
+ console.log(`Added deletedAt column to ${collectionName}`);
613
+ }
614
+ catch (error) {
615
+ // Column might already exist due to race condition, ignore
616
+ }
617
+ }
618
+ }
619
+ /**
620
+ * Create table from schema definition using raw SQL
621
+ */
622
+ async createTableFromSchema(collectionName, schema, skipFKConstraints = false) {
623
+ const sql = getSqlClient();
624
+ // Check if table already exists
625
+ const exists = await sql `
626
+ SELECT EXISTS (
627
+ SELECT FROM information_schema.tables
628
+ WHERE table_name = ${collectionName}
629
+ )
630
+ `;
631
+ if (exists[0].exists) {
632
+ console.log(`Table ${collectionName} already exists, syncing schema changes`);
633
+ // Sync missing columns with existing table
634
+ await this.syncTableColumns(collectionName, schema);
635
+ // Check/add foreign key constraints
636
+ if (!skipFKConstraints) {
637
+ await this.ensureForeignKeyConstraints(collectionName, schema);
638
+ }
639
+ return;
640
+ }
641
+ // Build CREATE TABLE statement
642
+ const columns = [];
643
+ const foreignKeyAssociations = [];
644
+ for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
645
+ const fs = fieldSchema;
646
+ // Handle BelongsTo relations - they need a foreign key column
647
+ if (fs.relType === 'BelongsTo') {
648
+ const foreignKey = fs.foreignKey || `${fieldName}_Id`;
649
+ // Check if foreign key column already exists as a separate field
650
+ // If foreignKey === fieldName AND field has explicit type, we'll create it below
651
+ const foreignKeyExists = foreignKey !== fieldName && Object.keys(schema.fields).includes(foreignKey);
652
+ if (!foreignKeyExists && foreignKey !== fieldName) {
653
+ // Only create foreign key column if it doesn't already exist as a separate field
654
+ const columnDef = this.buildColumnDefinition(foreignKey, {
655
+ type: fs.type || 'UUID',
656
+ allowNull: fs.allowNull,
657
+ unique: fs.unique
658
+ });
659
+ if (columnDef) {
660
+ columns.push(columnDef);
661
+ }
662
+ }
663
+ // Store association for later foreign key constraint creation
664
+ foreignKeyAssociations.push({ fieldName, assoc: fs });
665
+ // If foreignKey === fieldName AND field has explicit type, don't skip - create column below
666
+ if (foreignKey === fieldName && fs.type) {
667
+ // Fall through to create the column
668
+ }
669
+ else {
670
+ continue;
671
+ }
672
+ }
673
+ // Skip other relation types that don't have explicit type
674
+ if (fs.relType && !fs.type)
675
+ continue;
676
+ const columnDef = this.buildColumnDefinition(fieldName, fs);
677
+ if (columnDef) {
678
+ columns.push(columnDef);
679
+ }
680
+ }
681
+ // Add timestamps if enabled (default: true unless explicitly set to false)
682
+ if (schema.timestamps !== false) {
683
+ if (!schema.fields.createdAt) {
684
+ columns.push('"createdAt" TIMESTAMPTZ DEFAULT NOW()');
685
+ }
686
+ if (!schema.fields.updatedAt) {
687
+ columns.push('"updatedAt" TIMESTAMPTZ DEFAULT NOW()');
688
+ }
689
+ }
690
+ // Add deletedAt for paranoid mode
691
+ if (schema.paranoid) {
692
+ if (!schema.fields.deletedAt) {
693
+ columns.push('"deletedAt" TIMESTAMPTZ');
694
+ }
695
+ }
696
+ if (columns.length === 0) {
697
+ console.warn(`No columns to create for table ${collectionName}`);
698
+ return;
699
+ }
700
+ const createTableSQL = `CREATE TABLE "${collectionName}" (${columns.join(', ')})`;
701
+ try {
702
+ await sql.unsafe(createTableSQL);
703
+ console.log(`Created table: ${collectionName}`);
704
+ // Create foreign key constraints for BelongsTo relations (unless skipped)
705
+ if (!skipFKConstraints && foreignKeyAssociations.length > 0) {
706
+ await this.ensureForeignKeyConstraints(collectionName, schema);
707
+ }
708
+ // Create indexes if defined in schema
709
+ if (schema.indexes && Array.isArray(schema.indexes)) {
710
+ for (const index of schema.indexes) {
711
+ await this.createIndex(collectionName, index);
712
+ }
713
+ }
714
+ }
715
+ catch (error) {
716
+ console.error(`Failed to create table ${collectionName}:`, error);
717
+ }
718
+ }
719
+ /**
720
+ * Ensure foreign key constraints exist for BelongsTo relations
721
+ */
722
+ async ensureForeignKeyConstraints(collectionName, schema) {
723
+ const sql = getSqlClient();
724
+ // Extract BelongsTo relations from schema fields
725
+ const belongsToRelations = [];
726
+ for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
727
+ const fs = fieldSchema;
728
+ if (fs.relType === 'BelongsTo') {
729
+ belongsToRelations.push({ fieldName, assoc: fs });
730
+ }
731
+ }
732
+ if (belongsToRelations.length === 0) {
733
+ return; // No BelongsTo relations
734
+ }
735
+ // Track if any columns were added
736
+ let columnsAdded = false;
737
+ // Process each BelongsTo relation
738
+ for (const { fieldName, assoc } of belongsToRelations) {
739
+ // Skip if constraints are explicitly disabled (for polymorphic relations)
740
+ if (assoc.constraints === false) {
741
+ console.log(`Skipping FK constraint for ${fieldName} (constraints: false)`);
742
+ continue;
743
+ }
744
+ const foreignKey = assoc.foreignKey || `${fieldName}_Id`;
745
+ const targetTable = assoc.target;
746
+ const targetKey = assoc.targetKey || 'id';
747
+ const onDelete = (assoc.onDelete || 'CASCADE').toUpperCase();
748
+ const onUpdate = (assoc.onUpdate || 'CASCADE').toUpperCase();
749
+ const constraintName = `fk_${collectionName}_${foreignKey}`;
750
+ try {
751
+ // First, check if the foreign key column exists
752
+ const columnExists = await sql `
753
+ SELECT column_name
754
+ FROM information_schema.columns
755
+ WHERE table_name = ${collectionName}
756
+ AND column_name = ${foreignKey}
757
+ `;
758
+ if (columnExists.length === 0) {
759
+ // Column doesn't exist, add it
760
+ console.log(`Adding foreign key column ${foreignKey} to ${collectionName}`);
761
+ // Get the type from the foreign key field definition, not the relation definition
762
+ const columnType = schema.fields[foreignKey]?.type || assoc.type || 'UUID';
763
+ const pgType = columnType === 'UUID' ? 'UUID' :
764
+ columnType === 'Integer' ? 'INTEGER' :
765
+ columnType === 'String' ? 'TEXT' : 'UUID';
766
+ // Use IF NOT EXISTS for safety
767
+ await sql.unsafe(`ALTER TABLE "${collectionName}" ADD COLUMN IF NOT EXISTS "${foreignKey}" ${pgType}`);
768
+ console.log(`Added column ${foreignKey} to ${collectionName}`);
769
+ columnsAdded = true;
770
+ // Update schema definition to include the new field
771
+ if (!schema.fields[foreignKey]) {
772
+ schema.fields[foreignKey] = {
773
+ type: columnType,
774
+ allowNull: true,
775
+ SystemGenerated: true
776
+ };
777
+ }
778
+ }
779
+ // Check if constraint already exists
780
+ const existingConstraint = await sql `
781
+ SELECT
782
+ tc.constraint_name,
783
+ rc.delete_rule,
784
+ rc.update_rule
785
+ FROM information_schema.table_constraints tc
786
+ JOIN information_schema.referential_constraints rc
787
+ ON tc.constraint_name = rc.constraint_name
788
+ WHERE tc.table_name = ${collectionName}
789
+ AND tc.constraint_type = 'FOREIGN KEY'
790
+ AND tc.constraint_name = ${constraintName}
791
+ `;
792
+ // If constraint exists, check if onDelete/onUpdate actions match
793
+ if (existingConstraint.length > 0) {
794
+ const existing = existingConstraint[0];
795
+ const existingOnDelete = existing.delete_rule.replace(' ', '_').toUpperCase();
796
+ const existingOnUpdate = existing.update_rule.replace(' ', '_').toUpperCase();
797
+ if (existingOnDelete === onDelete && existingOnUpdate === onUpdate) {
798
+ console.log(`Foreign key constraint ${constraintName} already exists with correct actions`);
799
+ continue; // Constraint is correct, skip
800
+ }
801
+ // Drop the old constraint if actions don't match
802
+ console.log(`Dropping foreign key constraint ${constraintName} to update actions`);
803
+ await sql.unsafe(`ALTER TABLE "${collectionName}" DROP CONSTRAINT "${constraintName}"`);
804
+ }
805
+ // Create the foreign key constraint
806
+ const fkSQL = createForeignKeySQL(collectionName, foreignKey, targetTable, targetKey, onDelete, onUpdate);
807
+ await sql.unsafe(fkSQL);
808
+ console.log(`Created foreign key constraint: ${constraintName}`);
809
+ }
810
+ catch (error) {
811
+ console.error(`Failed to create foreign key constraint ${constraintName}:`, error);
812
+ // Don't throw - allow table creation to continue even if FK constraint fails
813
+ }
814
+ }
815
+ // If columns were added, regenerate the Drizzle schema to include them
816
+ if (columnsAdded) {
817
+ console.log(`Regenerating Drizzle schema for ${collectionName} to include new foreign key columns`);
818
+ console.log(`Fields in schema:`, Object.keys(schema.fields));
819
+ // Update schemaDefinitions Map with the modified schema
820
+ const schemaDef = this.schemaDefinitions.get(collectionName);
821
+ if (schemaDef) {
822
+ schemaDef.schema = schema;
823
+ this.schemaDefinitions.set(collectionName, schemaDef);
824
+ }
825
+ // Update the schema in the database table
826
+ const db = getDatabase();
827
+ await db
828
+ .update(baasixSchemaDefinition)
829
+ .set({ schema: schema })
830
+ .where(eq(baasixSchemaDefinition.collectionName, collectionName));
831
+ console.log(`Updated schema definition in database for ${collectionName}`);
832
+ // Regenerate the Drizzle table schema
833
+ await this.createOrUpdateModel(collectionName, schema);
834
+ const updatedSchema = this.schemas.get(collectionName);
835
+ console.log(`After regeneration, ${collectionName} table has columns:`, Object.keys(updatedSchema || {}).filter(k => !k.startsWith('_')));
836
+ console.log(`Drizzle schema for ${collectionName} regenerated successfully`);
837
+ }
838
+ }
839
+ /**
840
+ * Create an index on a table
841
+ */
842
+ async createIndex(tableName, indexDef) {
843
+ const sql = getSqlClient();
844
+ try {
845
+ const fields = indexDef.fields.map((f) => `"${f}"`).join(', ');
846
+ const indexName = indexDef.name || `${tableName}_${indexDef.fields.join('_')}_idx`;
847
+ const unique = indexDef.unique ? 'UNIQUE' : '';
848
+ // Support NULLS NOT DISTINCT for unique indexes (PostgreSQL 15+)
849
+ let nullsNotDistinct = '';
850
+ if (indexDef.unique && indexDef.nullsNotDistinct) {
851
+ const supportsNullsNotDistinct = await isPgVersionAtLeast(15);
852
+ if (supportsNullsNotDistinct) {
853
+ nullsNotDistinct = ' NULLS NOT DISTINCT';
854
+ }
855
+ else {
856
+ console.warn(`Index ${indexName}: NULLS NOT DISTINCT requires PostgreSQL 15+, ignoring option`);
857
+ }
858
+ }
859
+ const createIndexSQL = `CREATE ${unique} INDEX IF NOT EXISTS "${indexName}" ON "${tableName}" (${fields})${nullsNotDistinct}`;
860
+ await sql.unsafe(createIndexSQL);
861
+ console.log(`Created index: ${indexName} on ${tableName}`);
862
+ }
863
+ catch (error) {
864
+ console.error(`Failed to create index on ${tableName}:`, error);
865
+ }
866
+ }
867
+ /**
868
+ * Build column definition for CREATE TABLE
869
+ */
870
+ buildColumnDefinition(fieldName, fieldSchema) {
871
+ const parts = [`"${fieldName}"`];
872
+ // Handle VIRTUAL (computed) fields - these are GENERATED columns
873
+ if (fieldSchema.type === 'VIRTUAL') {
874
+ if (fieldSchema.calculated) {
875
+ // VIRTUAL fields are GENERATED ALWAYS AS ... STORED
876
+ parts.push('TEXT'); // Default to TEXT for computed fields
877
+ parts.push(`GENERATED ALWAYS AS (${fieldSchema.calculated}) STORED`);
878
+ return parts.join(' ');
879
+ }
880
+ else {
881
+ console.warn(`VIRTUAL field "${fieldName}" has no calculated expression. Skipping.`);
882
+ return null;
883
+ }
884
+ }
885
+ // Check for AUTOINCREMENT first
886
+ const hasAutoIncrement = fieldSchema.defaultValue?.type === 'AUTOINCREMENT';
887
+ // Map type
888
+ let pgType = 'TEXT';
889
+ switch (fieldSchema.type) {
890
+ case 'UUID':
891
+ pgType = 'UUID';
892
+ break;
893
+ case 'String':
894
+ pgType = fieldSchema.values?.stringLength ? `VARCHAR(${fieldSchema.values.stringLength})` : 'TEXT';
895
+ break;
896
+ case 'Text':
897
+ pgType = 'TEXT';
898
+ break;
899
+ case 'HTML':
900
+ // HTML content - stored as TEXT in database
901
+ pgType = 'TEXT';
902
+ break;
903
+ case 'Integer':
904
+ // Use SERIAL for auto-increment integers
905
+ pgType = hasAutoIncrement ? 'SERIAL' : 'INTEGER';
906
+ break;
907
+ case 'BigInt':
908
+ // Use BIGSERIAL for auto-increment bigints
909
+ pgType = hasAutoIncrement ? 'BIGSERIAL' : 'BIGINT';
910
+ break;
911
+ case 'Boolean':
912
+ pgType = 'BOOLEAN';
913
+ break;
914
+ case 'DateTime':
915
+ pgType = 'TIMESTAMPTZ';
916
+ break;
917
+ case 'DateTime_NO_TZ':
918
+ pgType = 'TIMESTAMP';
919
+ break;
920
+ case 'Date':
921
+ pgType = 'DATE';
922
+ break;
923
+ case 'Time':
924
+ pgType = 'TIMETZ';
925
+ break;
926
+ case 'Time_NO_TZ':
927
+ pgType = 'TIME';
928
+ break;
929
+ case 'JSON':
930
+ case 'JSONB':
931
+ pgType = 'JSONB';
932
+ break;
933
+ case 'Decimal':
934
+ case 'Real':
935
+ case 'Double':
936
+ pgType = 'NUMERIC';
937
+ break;
938
+ // PostGIS Geometry types
939
+ case 'Point':
940
+ pgType = `geometry(Point, ${fieldSchema.values?.srid || 4326})`;
941
+ break;
942
+ case 'LineString':
943
+ pgType = `geometry(LineString, ${fieldSchema.values?.srid || 4326})`;
944
+ break;
945
+ case 'Polygon':
946
+ pgType = `geometry(Polygon, ${fieldSchema.values?.srid || 4326})`;
947
+ break;
948
+ case 'MultiPoint':
949
+ pgType = `geometry(MultiPoint, ${fieldSchema.values?.srid || 4326})`;
950
+ break;
951
+ case 'MultiLineString':
952
+ pgType = `geometry(MultiLineString, ${fieldSchema.values?.srid || 4326})`;
953
+ break;
954
+ case 'MultiPolygon':
955
+ pgType = `geometry(MultiPolygon, ${fieldSchema.values?.srid || 4326})`;
956
+ break;
957
+ case 'GeometryCollection':
958
+ pgType = `geometry(GeometryCollection, ${fieldSchema.values?.srid || 4326})`;
959
+ break;
960
+ case 'Geography':
961
+ pgType = `geography(Point, ${fieldSchema.values?.srid || 4326})`;
962
+ break;
963
+ }
964
+ parts.push(pgType);
965
+ // Primary key
966
+ if (fieldSchema.primaryKey) {
967
+ parts.push('PRIMARY KEY');
968
+ }
969
+ // Not null
970
+ if (fieldSchema.allowNull === false) {
971
+ parts.push('NOT NULL');
972
+ }
973
+ // Unique
974
+ if (fieldSchema.unique) {
975
+ parts.push('UNIQUE');
976
+ }
977
+ // Default value (skip if AUTOINCREMENT as SERIAL handles it)
978
+ if (fieldSchema.defaultValue !== undefined && !hasAutoIncrement) {
979
+ if (typeof fieldSchema.defaultValue === 'object' && fieldSchema.defaultValue.type) {
980
+ switch (fieldSchema.defaultValue.type) {
981
+ case 'UUIDV4':
982
+ parts.push('DEFAULT gen_random_uuid()');
983
+ break;
984
+ case 'SUID':
985
+ // Short unique ID - uses gen_random_uuid() for now
986
+ parts.push('DEFAULT gen_random_uuid()');
987
+ break;
988
+ case 'NOW':
989
+ parts.push('DEFAULT NOW()');
990
+ break;
991
+ case 'SQL':
992
+ // Raw SQL default expression
993
+ if (fieldSchema.defaultValue.value) {
994
+ parts.push(`DEFAULT ${fieldSchema.defaultValue.value}`);
995
+ }
996
+ break;
997
+ }
998
+ }
999
+ else if (typeof fieldSchema.defaultValue === 'string') {
1000
+ parts.push(`DEFAULT '${fieldSchema.defaultValue}'`);
1001
+ }
1002
+ else if (typeof fieldSchema.defaultValue === 'number') {
1003
+ parts.push(`DEFAULT ${fieldSchema.defaultValue}`);
1004
+ }
1005
+ else if (typeof fieldSchema.defaultValue === 'boolean') {
1006
+ parts.push(`DEFAULT ${fieldSchema.defaultValue}`);
1007
+ }
1008
+ }
1009
+ return parts.join(' ');
1010
+ }
1011
+ /**
1012
+ * Check if database is empty and seed if needed
1013
+ */
1014
+ async checkAndSeedDatabase() {
1015
+ // Check if we have the necessary tables
1016
+ const userSchema = this.schemas.get('baasix_User');
1017
+ const roleSchema = this.schemas.get('baasix_Role');
1018
+ if (!userSchema || !roleSchema) {
1019
+ return;
1020
+ }
1021
+ const sql = getSqlClient();
1022
+ // Count users and roles
1023
+ const userCount = await sql `SELECT COUNT(*) FROM "baasix_User"`;
1024
+ const roleCount = await sql `SELECT COUNT(*) FROM "baasix_Role"`;
1025
+ if (parseInt(userCount[0].count) === 0 && parseInt(roleCount[0].count) === 0) {
1026
+ console.log('Database is empty, seeding...');
1027
+ await this.seedDatabase();
1028
+ }
1029
+ }
1030
+ /**
1031
+ * Seed the database with initial data
1032
+ */
1033
+ async seedDatabase() {
1034
+ const sql = getSqlClient();
1035
+ try {
1036
+ console.log('Starting seeding...');
1037
+ // Create default roles
1038
+ await sql `
1039
+ INSERT INTO "baasix_Role" (id, name, description, "isTenantSpecific")
1040
+ VALUES
1041
+ (gen_random_uuid(), 'administrator', 'Full system access', false),
1042
+ (gen_random_uuid(), 'user', 'Standard user access', true),
1043
+ (gen_random_uuid(), 'public', 'Public access (unauthenticated)', true)
1044
+ ON CONFLICT (name) DO NOTHING
1045
+ `;
1046
+ console.log('Default roles created');
1047
+ // Get admin role ID
1048
+ const adminRole = await sql `
1049
+ SELECT id FROM "baasix_Role" WHERE name = 'administrator' LIMIT 1
1050
+ `;
1051
+ if (adminRole.length > 0) {
1052
+ const adminRoleId = adminRole[0].id;
1053
+ // Hash the admin password
1054
+ const hashedPassword = await argon2.hash('admin@123');
1055
+ // Create default admin user
1056
+ const adminUserId = await sql `
1057
+ INSERT INTO "baasix_User" (id, email, "firstName", "lastName", password)
1058
+ VALUES (gen_random_uuid(), 'admin@baasix.com', 'Baasix', 'Admin', ${hashedPassword})
1059
+ ON CONFLICT (email) DO NOTHING
1060
+ RETURNING id
1061
+ `;
1062
+ if (adminUserId.length > 0) {
1063
+ // Assign admin role to admin user
1064
+ await sql `
1065
+ INSERT INTO "baasix_UserRole" (id, "user_Id", "role_Id", "tenant_Id")
1066
+ VALUES (gen_random_uuid(), ${adminUserId[0].id}, ${adminRoleId}, NULL)
1067
+ ON CONFLICT DO NOTHING
1068
+ `;
1069
+ }
1070
+ console.log('Default admin user created');
1071
+ }
1072
+ // Seed default email templates
1073
+ await this.seedDefaultTemplates();
1074
+ console.log('Seeding complete');
1075
+ }
1076
+ catch (error) {
1077
+ console.error('Error seeding database:', error);
1078
+ }
1079
+ }
1080
+ /**
1081
+ * Seed default email templates into baasix_Template table
1082
+ */
1083
+ async seedDefaultTemplates() {
1084
+ const sql = getSqlClient();
1085
+ // Check if templates table exists
1086
+ const templateTableExists = await sql `
1087
+ SELECT EXISTS (
1088
+ SELECT FROM information_schema.tables
1089
+ WHERE table_name = 'baasix_Template'
1090
+ )
1091
+ `;
1092
+ if (!templateTableExists[0].exists) {
1093
+ console.log('baasix_Template table does not exist yet, skipping template seeding');
1094
+ return;
1095
+ }
1096
+ const defaultTemplates = [
1097
+ {
1098
+ type: 'inviteNewUser',
1099
+ subject: "You've been invited to join {{ tenant }}",
1100
+ body: `<h2>Welcome!</h2>
1101
+ <p>Hi,</p>
1102
+ <p>You've been invited by <strong>{{ inviterName }}</strong> to join <strong>{{ tenant }}</strong>.</p>
1103
+ <p>Click the button below to accept your invitation and create your account:</p>
1104
+ <p style="text-align: center; margin: 30px 0;">
1105
+ <a href="{{ inviteLink }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Accept Invitation</a>
1106
+ </p>
1107
+ <p><strong>Note:</strong> This invitation will expire on {{ expirationDate }}.</p>
1108
+ <p>If you didn't expect this invitation, you can safely ignore this email.</p>`,
1109
+ description: 'Template for inviting new users who do not have an account yet'
1110
+ },
1111
+ {
1112
+ type: 'inviteExistingUser',
1113
+ subject: "You've been invited to join {{ tenant }}",
1114
+ body: `<h2>New Invitation</h2>
1115
+ <p>Hi,</p>
1116
+ <p>You've been invited by <strong>{{ inviterName }}</strong> to join <strong>{{ tenant }}</strong>.</p>
1117
+ <p>Since you already have an account, click the button below to accept the invitation:</p>
1118
+ <p style="text-align: center; margin: 30px 0;">
1119
+ <a href="{{ inviteLink }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Accept Invitation</a>
1120
+ </p>
1121
+ <p><strong>Note:</strong> This invitation will expire on {{ expirationDate }}.</p>
1122
+ <p>If you didn't expect this invitation, you can safely ignore this email.</p>`,
1123
+ description: 'Template for inviting existing users to a new tenant'
1124
+ },
1125
+ {
1126
+ type: 'magicLinkUrl',
1127
+ subject: 'Sign in to {{ project_name }}',
1128
+ body: `<h2>Sign In Request</h2>
1129
+ <p>Hi {{ name }},</p>
1130
+ <p>Click the button below to sign in to your account:</p>
1131
+ <p style="text-align: center; margin: 30px 0;">
1132
+ <a href="{{ magicLinkUrl }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Sign In</a>
1133
+ </p>
1134
+ <p>This link will expire in 15 minutes for security purposes.</p>
1135
+ <p>If you didn't request this sign-in link, you can safely ignore this email.</p>`,
1136
+ description: 'Template for magic link URL authentication'
1137
+ },
1138
+ {
1139
+ type: 'magicLinkCode',
1140
+ subject: 'Your sign in code for {{ project_name }}',
1141
+ body: `<h2>Sign In Code</h2>
1142
+ <p>Hi {{ name }},</p>
1143
+ <p>Use the following code to sign in to your account:</p>
1144
+ <p style="text-align: center; margin: 30px 0;">
1145
+ <span style="background-color: #f5f5f5; padding: 16px 32px; font-size: 24px; font-family: monospace; letter-spacing: 4px; border-radius: 4px; display: inline-block;">{{ code }}</span>
1146
+ </p>
1147
+ <p>This code will expire in 15 minutes for security purposes.</p>
1148
+ <p>If you didn't request this code, you can safely ignore this email.</p>`,
1149
+ description: 'Template for magic link code authentication'
1150
+ },
1151
+ {
1152
+ type: 'passwordReset',
1153
+ subject: 'Reset your password for {{ project_name }}',
1154
+ body: `<h2>Password Reset</h2>
1155
+ <p>Hi {{ name }},</p>
1156
+ <p>We received a request to reset your password. Click the button below to choose a new password:</p>
1157
+ <p style="text-align: center; margin: 30px 0;">
1158
+ <a href="{{ resetUrl }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Reset Password</a>
1159
+ </p>
1160
+ <p>This link will expire in 1 hour for security purposes.</p>
1161
+ <p>If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.</p>`,
1162
+ description: 'Template for password reset emails'
1163
+ },
1164
+ {
1165
+ type: 'emailVerification',
1166
+ subject: 'Verify your email for {{ project_name }}',
1167
+ body: `<h2>Email Verification</h2>
1168
+ <p>Hi {{ name }},</p>
1169
+ <p>Please verify your email address by clicking the button below:</p>
1170
+ <p style="text-align: center; margin: 30px 0;">
1171
+ <a href="{{ verifyUrl }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Verify Email</a>
1172
+ </p>
1173
+ <p>This link will expire in 24 hours.</p>
1174
+ <p>If you didn't create an account, you can safely ignore this email.</p>`,
1175
+ description: 'Template for email verification'
1176
+ },
1177
+ {
1178
+ type: 'welcome',
1179
+ subject: 'Welcome to {{ project_name }}!',
1180
+ body: `<h2>Welcome!</h2>
1181
+ <p>Hi {{ name }},</p>
1182
+ <p>Thank you for joining {{ project_name }}! We're excited to have you on board.</p>
1183
+ <p>Your account has been successfully created and you're ready to get started.</p>
1184
+ <p style="text-align: center; margin: 30px 0;">
1185
+ <a href="{{ loginUrl }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Get Started</a>
1186
+ </p>
1187
+ <p>If you have any questions, feel free to reach out to our support team.</p>`,
1188
+ description: 'Template for welcome emails to new users'
1189
+ },
1190
+ {
1191
+ type: 'notification',
1192
+ subject: '{{ notification_title }}',
1193
+ body: `<h2>{{ notification_title }}</h2>
1194
+ <p>Hi {{ name }},</p>
1195
+ <div>{{ notification_message }}</div>
1196
+ {% if action_url %}
1197
+ <p style="text-align: center; margin: 30px 0;">
1198
+ <a href="{{ action_url }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">{{ action_text | default: 'View Details' }}</a>
1199
+ </p>
1200
+ {% endif %}`,
1201
+ description: 'Generic notification template'
1202
+ }
1203
+ ];
1204
+ try {
1205
+ for (const template of defaultTemplates) {
1206
+ await sql `
1207
+ INSERT INTO "baasix_Template" (id, type, subject, body, "tenant_Id", "isActive", description)
1208
+ VALUES (gen_random_uuid(), ${template.type}, ${template.subject}, ${template.body}, NULL, true, ${template.description})
1209
+ ON CONFLICT ("tenant_Id", type) DO NOTHING
1210
+ `;
1211
+ }
1212
+ console.log('Default email templates created');
1213
+ }
1214
+ catch (error) {
1215
+ console.error('Error seeding default templates:', error);
1216
+ }
1217
+ }
1218
+ /**
1219
+ * Ensure baasix_SchemaDefinition table exists
1220
+ */
1221
+ async ensureSchemaDefinitionTableOLD() {
1222
+ const sql = getSqlClient();
1223
+ // Check if table exists
1224
+ const result = await sql `
1225
+ SELECT EXISTS (
1226
+ SELECT FROM information_schema.tables
1227
+ WHERE table_name = 'baasix_SchemaDefinition'
1228
+ )
1229
+ `;
1230
+ if (!result[0].exists) {
1231
+ console.log('Creating baasix_SchemaDefinition table...');
1232
+ await sql `
1233
+ CREATE TABLE "baasix_SchemaDefinition" (
1234
+ id SERIAL PRIMARY KEY,
1235
+ "collectionName" TEXT NOT NULL UNIQUE,
1236
+ schema JSONB NOT NULL,
1237
+ active BOOLEAN NOT NULL DEFAULT true,
1238
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1239
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1240
+ "deletedAt" TIMESTAMPTZ
1241
+ )
1242
+ `;
1243
+ }
1244
+ }
1245
+ /**
1246
+ * Create or update a model from JSON schema definition
1247
+ */
1248
+ async createOrUpdateModel(collectionName, jsonSchema) {
1249
+ try {
1250
+ console.log(`Creating/updating model: ${collectionName}`);
1251
+ let { fields, options, associations } = jsonSchema;
1252
+ // Add tenant fields for non-system schemas in multi-tenant mode
1253
+ const isSystemSchema = collectionName.startsWith('baasix_');
1254
+ const envValue = env.get('MULTI_TENANT');
1255
+ const isMultiTenant = envValue === 'true';
1256
+ console.log(`[createOrUpdateModel] ${collectionName}:`, {
1257
+ isSystemSchema,
1258
+ envValue: `"${envValue}"`,
1259
+ isMultiTenant,
1260
+ willAddTenantFields: isMultiTenant && !isSystemSchema
1261
+ });
1262
+ if (isMultiTenant && !isSystemSchema) {
1263
+ console.log(`[createOrUpdateModel] Adding tenant fields to ${collectionName}`);
1264
+ // Add tenant_Id field and tenant relation for multi-tenant isolation
1265
+ // IMPORTANT: Modify jsonSchema.fields directly so changes are reflected in createTableFromSchema
1266
+ jsonSchema.fields = {
1267
+ ...fields,
1268
+ tenant_Id: {
1269
+ type: 'UUID',
1270
+ allowNull: true,
1271
+ SystemGenerated: 'true',
1272
+ description: 'Tenant identifier for multi-tenant isolation'
1273
+ },
1274
+ tenant: {
1275
+ relType: 'BelongsTo',
1276
+ target: 'baasix_Tenant',
1277
+ foreignKey: 'tenant_Id',
1278
+ as: 'tenant',
1279
+ SystemGenerated: 'true',
1280
+ description: 'M2O relationship to tenant'
1281
+ }
1282
+ };
1283
+ // Update local fields variable to match
1284
+ fields = jsonSchema.fields;
1285
+ // Add tenant_Id to unique indexes for proper multi-tenant isolation
1286
+ if (!options) {
1287
+ options = {};
1288
+ jsonSchema.options = options;
1289
+ }
1290
+ if (!options.indexes) {
1291
+ options.indexes = [];
1292
+ }
1293
+ options.indexes = options.indexes.map((index) => {
1294
+ if (index.unique && !index.fields.includes('tenant_Id')) {
1295
+ return {
1296
+ ...index,
1297
+ fields: [...index.fields, 'tenant_Id']
1298
+ };
1299
+ }
1300
+ return index;
1301
+ });
1302
+ // Update jsonSchema.options to reflect index changes
1303
+ jsonSchema.options = options;
1304
+ }
1305
+ // Extract associations from fields if not provided separately
1306
+ // This maintains compatibility with Sequelize-style schemas where relations are in fields
1307
+ if (!associations) {
1308
+ associations = {};
1309
+ console.log(`[createOrUpdateModel] Extracting associations for ${collectionName} from fields:`, Object.keys(fields));
1310
+ for (const [fieldName, fieldSchema] of Object.entries(fields)) {
1311
+ if (isRelationField(fieldSchema)) {
1312
+ const relSchema = fieldSchema;
1313
+ console.log(`[createOrUpdateModel] Found relation field ${fieldName}:`, { relType: relSchema.relType, target: relSchema.target, polymorphic: relSchema.polymorphic });
1314
+ // Use the 'as' name as the key if provided, otherwise use fieldName
1315
+ // This allows relations to be accessed by their alias (e.g., 'category' instead of 'categoryId')
1316
+ const relationKey = relSchema.as || fieldName;
1317
+ associations[relationKey] = {
1318
+ type: relSchema.relType,
1319
+ model: relSchema.target,
1320
+ foreignKey: relSchema.foreignKey,
1321
+ targetKey: relSchema.targetKey,
1322
+ as: relSchema.as || fieldName,
1323
+ // For M2A (polymorphic), target IS the junction table
1324
+ // For BelongsToMany, through is explicitly set
1325
+ through: relSchema.through || (relSchema.polymorphic ? relSchema.target : undefined),
1326
+ onDelete: relSchema.onDelete,
1327
+ onUpdate: relSchema.onUpdate,
1328
+ // M2A/polymorphic specific fields
1329
+ polymorphic: relSchema.polymorphic,
1330
+ tables: relSchema.tables
1331
+ };
1332
+ }
1333
+ }
1334
+ console.log(`[createOrUpdateModel] Extracted ${Object.keys(associations).length} associations for ${collectionName}:`, Object.keys(associations));
1335
+ }
1336
+ // Build column definitions
1337
+ const columns = {};
1338
+ console.log(`[createOrUpdateModel] ${collectionName} field names after tenant injection:`, Object.keys(fields));
1339
+ // Process each field
1340
+ for (const [fieldName, fieldSchema] of Object.entries(fields)) {
1341
+ // Skip relationship-only fields (no explicit type defined)
1342
+ // But process fields that have both type AND relType (e.g., foreign key columns)
1343
+ if (isRelationField(fieldSchema) && !fieldSchema.type) {
1344
+ continue;
1345
+ }
1346
+ try {
1347
+ const column = mapJsonTypeToDrizzle(fieldName, fieldSchema);
1348
+ if (column) {
1349
+ columns[fieldName] = column;
1350
+ }
1351
+ }
1352
+ catch (error) {
1353
+ console.warn(`Failed to map field ${fieldName}:`, error);
1354
+ }
1355
+ }
1356
+ // Add timestamps if enabled (default: true)
1357
+ const includeTimestamps = options?.timestamps !== false;
1358
+ if (includeTimestamps) {
1359
+ if (!columns.createdAt) {
1360
+ columns.createdAt = timestamp('createdAt', { withTimezone: true }).notNull().defaultNow();
1361
+ }
1362
+ if (!columns.updatedAt) {
1363
+ columns.updatedAt = timestamp('updatedAt', { withTimezone: true }).notNull().defaultNow();
1364
+ }
1365
+ }
1366
+ // Add deletedAt for paranoid mode
1367
+ if (options?.paranoid) {
1368
+ if (!columns.deletedAt) {
1369
+ columns.deletedAt = timestamp('deletedAt', { withTimezone: true });
1370
+ }
1371
+ }
1372
+ // Create the table schema
1373
+ const tableSchema = pgTable(collectionName, columns);
1374
+ // Store the schema (soft-delete filtering will be applied at query time)
1375
+ this.schemas.set(collectionName, tableSchema);
1376
+ // Track paranoid mode for this table
1377
+ if (options?.paranoid) {
1378
+ this.schemas.set(`${collectionName}_paranoid`, true);
1379
+ }
1380
+ // Handle associations (store them for later query use)
1381
+ if (associations) {
1382
+ relationBuilder.storeAssociations(collectionName, associations);
1383
+ }
1384
+ // NOTE: Index creation is now handled in createTableFromSchema
1385
+ // to avoid DB queries during fast startup path
1386
+ // Register hooks if needed
1387
+ this.registerModelHooks(collectionName, jsonSchema);
1388
+ console.log(`Model ${collectionName} created successfully`);
1389
+ return tableSchema;
1390
+ }
1391
+ catch (error) {
1392
+ console.error(`Failed to create/update model ${collectionName}:`, error);
1393
+ throw error;
1394
+ }
1395
+ }
1396
+ /**
1397
+ * Create indexes for a table
1398
+ */
1399
+ async createIndexes(tableName, indexes) {
1400
+ const sql = getSqlClient();
1401
+ // Check PostgreSQL version once for all indexes
1402
+ const supportsNullsNotDistinct = await isPgVersionAtLeast(15);
1403
+ for (const index of indexes) {
1404
+ try {
1405
+ const indexName = index.name || `${tableName}_${index.fields.join('_')}_idx`;
1406
+ const unique = index.unique ? 'UNIQUE' : '';
1407
+ const method = index.type || 'BTREE';
1408
+ const fields = index.fields.map(f => `"${f}"`).join(', ');
1409
+ // Support NULLS NOT DISTINCT for unique indexes (PostgreSQL 15+)
1410
+ let nullsNotDistinct = '';
1411
+ if (index.unique && index.nullsNotDistinct) {
1412
+ if (supportsNullsNotDistinct) {
1413
+ nullsNotDistinct = ' NULLS NOT DISTINCT';
1414
+ }
1415
+ else {
1416
+ console.warn(`Index ${indexName}: NULLS NOT DISTINCT requires PostgreSQL 15+, ignoring option`);
1417
+ }
1418
+ }
1419
+ // Check if index already exists
1420
+ const exists = await sql `
1421
+ SELECT EXISTS (
1422
+ SELECT FROM pg_indexes
1423
+ WHERE tablename = ${tableName}
1424
+ AND indexname = ${indexName}
1425
+ )
1426
+ `;
1427
+ if (!exists[0].exists) {
1428
+ await sql.unsafe(`
1429
+ CREATE ${unique} INDEX "${indexName}"
1430
+ ON "${tableName}" USING ${method} (${fields})${nullsNotDistinct}
1431
+ `);
1432
+ console.log(`Created index ${indexName} on ${tableName}`);
1433
+ }
1434
+ }
1435
+ catch (error) {
1436
+ console.warn(`Failed to create index on ${tableName}:`, error);
1437
+ }
1438
+ }
1439
+ }
1440
+ /**
1441
+ * Register model-specific hooks
1442
+ */
1443
+ registerModelHooks(collectionName, jsonSchema) {
1444
+ // Hook registration will be implemented based on schema configuration
1445
+ // For now, this is a placeholder for future hook registration
1446
+ // const hooksManager = HooksManager.getInstance();
1447
+ // Example: Register audit logging hook for all models
1448
+ // This can be customized based on schema options
1449
+ if (jsonSchema.options?.paranoid) {
1450
+ // Add soft-delete specific hooks if needed
1451
+ }
1452
+ }
1453
+ /**
1454
+ * Get a registered schema by collection name
1455
+ */
1456
+ getSchema(collectionName) {
1457
+ return this.schemas.get(collectionName);
1458
+ }
1459
+ /**
1460
+ * Get all registered schemas
1461
+ */
1462
+ getAllSchemas() {
1463
+ return this.schemas;
1464
+ }
1465
+ /**
1466
+ * Check if a model/collection exists
1467
+ */
1468
+ modelExists(collectionName) {
1469
+ return this.schemas.has(collectionName);
1470
+ }
1471
+ /**
1472
+ * Get table for a collection
1473
+ */
1474
+ getTable(collectionName) {
1475
+ const schema = this.schemas.get(collectionName);
1476
+ if (!schema) {
1477
+ throw new Error(`Table not found for collection: ${collectionName}`);
1478
+ }
1479
+ return schema;
1480
+ }
1481
+ /**
1482
+ * Get primary key field name for a collection
1483
+ */
1484
+ getPrimaryKey(collectionName) {
1485
+ // Get schema definition from schemaDefinitions Map (loaded during initialization)
1486
+ const schemaDef = this.schemaDefinitions.get(collectionName);
1487
+ if (schemaDef && schemaDef.schema && schemaDef.schema.fields) {
1488
+ // Find the field with primaryKey: true
1489
+ for (const [fieldName, fieldSchema] of Object.entries(schemaDef.schema.fields)) {
1490
+ if (fieldSchema.primaryKey === true) {
1491
+ return fieldName;
1492
+ }
1493
+ }
1494
+ }
1495
+ // Default to 'id' if no primary key is explicitly defined
1496
+ return 'id';
1497
+ }
1498
+ /**
1499
+ * Check if a collection has paranoid mode enabled (soft delete)
1500
+ */
1501
+ isParanoid(collectionName) {
1502
+ return this.schemas.get(`${collectionName}_paranoid`) === true;
1503
+ }
1504
+ /**
1505
+ * Get schema options for a collection
1506
+ */
1507
+ getSchemaOptions(collectionName) {
1508
+ const schema = this.getSchema(collectionName);
1509
+ if (!schema)
1510
+ return {};
1511
+ // Schema options are stored in the schema definition
1512
+ // For now, we track paranoid mode separately
1513
+ return {
1514
+ paranoid: this.isParanoid(collectionName)
1515
+ };
1516
+ }
1517
+ /**
1518
+ * Get schema definition with flags from baasix_SchemaDefinition table
1519
+ */
1520
+ async getSchemaDefinition(collectionName) {
1521
+ try {
1522
+ const schemaDefTable = this.getTable('baasix_SchemaDefinition');
1523
+ if (!schemaDefTable)
1524
+ return null;
1525
+ const db = getDatabase();
1526
+ const result = await db
1527
+ .select()
1528
+ .from(schemaDefTable)
1529
+ .where(eq(schemaDefTable.collectionName, collectionName))
1530
+ .limit(1);
1531
+ if (result.length === 0)
1532
+ return null;
1533
+ return result[0].schema;
1534
+ }
1535
+ catch (error) {
1536
+ console.error(`Error getting schema definition for ${collectionName}:`, error);
1537
+ return null;
1538
+ }
1539
+ }
1540
+ /**
1541
+ * Get relation names for a collection
1542
+ */
1543
+ getRelationNames(collectionName) {
1544
+ const associations = relationBuilder.getAssociations(collectionName);
1545
+ if (!associations)
1546
+ return [];
1547
+ return Object.keys(associations);
1548
+ }
1549
+ /**
1550
+ * Get relations for a collection
1551
+ */
1552
+ getRelations(collectionName) {
1553
+ return this.relations.get(collectionName);
1554
+ }
1555
+ /**
1556
+ * Get a specific relation for a collection
1557
+ */
1558
+ getRelation(collectionName, relationName) {
1559
+ const relations = this.relations.get(collectionName);
1560
+ return relations?.[relationName];
1561
+ }
1562
+ /**
1563
+ * Check if schema manager is initialized
1564
+ */
1565
+ isInitialized() {
1566
+ return this.initialized;
1567
+ }
1568
+ /**
1569
+ * Add a new schema definition to the database
1570
+ */
1571
+ async addSchemaDefinition(collectionName, schema) {
1572
+ const db = getDatabase();
1573
+ // Check if schema already exists
1574
+ const existing = await db
1575
+ .select()
1576
+ .from(baasixSchemaDefinition)
1577
+ .where(eq(baasixSchemaDefinition.collectionName, collectionName))
1578
+ .limit(1);
1579
+ if (existing.length > 0) {
1580
+ // Update existing schema
1581
+ await db
1582
+ .update(baasixSchemaDefinition)
1583
+ .set({
1584
+ schema: schema,
1585
+ updatedAt: new Date(),
1586
+ })
1587
+ .where(eq(baasixSchemaDefinition.collectionName, collectionName));
1588
+ }
1589
+ else {
1590
+ // Insert new schema
1591
+ await db.insert(baasixSchemaDefinition).values({
1592
+ collectionName,
1593
+ schema: schema,
1594
+ });
1595
+ }
1596
+ // Reload the schema
1597
+ await this.createOrUpdateModel(collectionName, schema);
1598
+ }
1599
+ /**
1600
+ * Remove a schema definition
1601
+ */
1602
+ async removeSchemaDefinition(collectionName) {
1603
+ const db = getDatabase();
1604
+ // Delete the schema definition
1605
+ await db
1606
+ .delete(baasixSchemaDefinition)
1607
+ .where(eq(baasixSchemaDefinition.collectionName, collectionName));
1608
+ // Remove from memory
1609
+ this.schemas.delete(collectionName);
1610
+ this.relations.delete(collectionName);
1611
+ }
1612
+ /**
1613
+ * Sync schemas - create/update tables in database
1614
+ * Similar to Sequelize.sync()
1615
+ */
1616
+ async sync(options) {
1617
+ console.log('Syncing schemas with database...');
1618
+ // For now, we rely on Drizzle Kit for migrations
1619
+ // In production, use: drizzle-kit push:pg or drizzle-kit migrate
1620
+ console.warn('Schema sync is handled by Drizzle Kit. Run: npm run db:push');
1621
+ }
1622
+ /**
1623
+ * Create or update a model (for schema routes compatibility)
1624
+ */
1625
+ async updateModel(collectionName, schema, accountability) {
1626
+ console.log(`Creating/updating model: ${collectionName}`);
1627
+ console.log(`[updateModel] Schema fields for ${collectionName}:`, Object.keys(schema.fields || {}));
1628
+ // Store JSON schema definition in memory
1629
+ this.schemaDefinitions.set(collectionName, { collectionName, schema });
1630
+ // Update schema definition in database
1631
+ const db = getDatabase();
1632
+ const existingSchema = await db
1633
+ .select()
1634
+ .from(baasixSchemaDefinition)
1635
+ .where(eq(baasixSchemaDefinition.collectionName, collectionName))
1636
+ .limit(1);
1637
+ if (existingSchema.length > 0) {
1638
+ // Update existing schema
1639
+ await db
1640
+ .update(baasixSchemaDefinition)
1641
+ .set({ schema: schema, updatedAt: new Date() })
1642
+ .where(eq(baasixSchemaDefinition.collectionName, collectionName));
1643
+ console.log(`Updated schema definition in database for ${collectionName}`);
1644
+ }
1645
+ else {
1646
+ // Insert new schema
1647
+ await db.insert(baasixSchemaDefinition).values({
1648
+ collectionName,
1649
+ schema: schema,
1650
+ });
1651
+ console.log(`Inserted new schema definition in database for ${collectionName}`);
1652
+ }
1653
+ // Create/update the Drizzle schema in memory
1654
+ await this.createOrUpdateModel(collectionName, schema);
1655
+ // Create the actual PostgreSQL table
1656
+ await this.createTableFromSchema(collectionName, schema);
1657
+ console.log(`Model ${collectionName} created/updated successfully`);
1658
+ }
1659
+ /**
1660
+ * Delete a model (for schema routes compatibility)
1661
+ */
1662
+ async deleteModel(collectionName) {
1663
+ console.log(`Deleting model: ${collectionName}`);
1664
+ this.schemas.delete(collectionName);
1665
+ // In production, this would drop the table
1666
+ }
1667
+ /**
1668
+ * Add an index to a collection
1669
+ */
1670
+ async addIndex(collectionName, indexData, accountability) {
1671
+ const sql = getSqlClient();
1672
+ try {
1673
+ const fields = indexData.fields;
1674
+ const indexName = indexData.name || `${collectionName}_${fields.join('_')}_idx`;
1675
+ const unique = indexData.unique ? 'UNIQUE' : '';
1676
+ // Support NULLS NOT DISTINCT for unique indexes (PostgreSQL 15+)
1677
+ let nullsNotDistinct = '';
1678
+ if (indexData.unique && indexData.nullsNotDistinct) {
1679
+ const supportsNullsNotDistinct = await isPgVersionAtLeast(15);
1680
+ if (supportsNullsNotDistinct) {
1681
+ nullsNotDistinct = ' NULLS NOT DISTINCT';
1682
+ }
1683
+ else {
1684
+ console.warn(`Index ${indexName}: NULLS NOT DISTINCT requires PostgreSQL 15+, ignoring option`);
1685
+ }
1686
+ }
1687
+ // Check if table exists
1688
+ const tableExists = await sql `
1689
+ SELECT EXISTS (
1690
+ SELECT FROM information_schema.tables
1691
+ WHERE table_name = ${collectionName}
1692
+ )
1693
+ `;
1694
+ if (!tableExists[0].exists) {
1695
+ throw new Error(`Table ${collectionName} does not exist`);
1696
+ }
1697
+ // Check if index already exists
1698
+ const indexExists = await sql `
1699
+ SELECT EXISTS (
1700
+ SELECT FROM pg_indexes
1701
+ WHERE tablename = ${collectionName}
1702
+ AND indexname = ${indexName}
1703
+ )
1704
+ `;
1705
+ if (indexExists[0].exists) {
1706
+ console.log(`Index ${indexName} already exists on ${collectionName}`);
1707
+ return;
1708
+ }
1709
+ // Build CREATE INDEX statement
1710
+ const fieldList = fields.map((f) => `"${f}"`).join(', ');
1711
+ const createIndexSQL = `CREATE ${unique} INDEX "${indexName}" ON "${collectionName}" (${fieldList})${nullsNotDistinct}`;
1712
+ await sql.unsafe(createIndexSQL);
1713
+ console.log(`Created index ${indexName} on ${collectionName}`);
1714
+ }
1715
+ catch (error) {
1716
+ console.error(`Failed to create index on ${collectionName}:`, error);
1717
+ throw error;
1718
+ }
1719
+ }
1720
+ /**
1721
+ * Remove an index (stub for compatibility)
1722
+ */
1723
+ async removeIndex(collectionName, indexName) {
1724
+ console.log(`Removing index ${indexName} from ${collectionName}`);
1725
+ // Stub for now
1726
+ }
1727
+ /**
1728
+ * Add missing foreign key indexes to all collections
1729
+ * Useful for migrating existing databases to use auto-indexing
1730
+ */
1731
+ async addMissingForeignKeyIndexes(accountability) {
1732
+ const sql = getSqlClient();
1733
+ const result = {
1734
+ created: [],
1735
+ skipped: [],
1736
+ errors: [],
1737
+ };
1738
+ console.log('Scanning for missing foreign key indexes...');
1739
+ // Fetch all schema definitions from database (not from memory cache which has Drizzle table objects)
1740
+ const schemaDefinitions = await sql `
1741
+ SELECT "collectionName", schema FROM "baasix_SchemaDefinition"
1742
+ `;
1743
+ console.log(`Found ${schemaDefinitions.length} schemas to scan for missing indexes`);
1744
+ // Track which schemas need to be updated
1745
+ const schemasToUpdate = new Map();
1746
+ for (const schemaDef of schemaDefinitions) {
1747
+ const collectionName = schemaDef.collectionName;
1748
+ const schema = schemaDef.schema;
1749
+ if (!schema?.fields)
1750
+ continue;
1751
+ const newIndexes = [];
1752
+ // Find all BelongsTo relationships (M2O, O2O)
1753
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
1754
+ const field = fieldDef;
1755
+ // Skip if field definition is null/undefined
1756
+ if (!field)
1757
+ continue;
1758
+ // Check if this is a BelongsTo relation
1759
+ if (field.relType === 'BelongsTo' && field.foreignKey) {
1760
+ const fkColumn = field.foreignKey;
1761
+ const indexName = `${collectionName}_${fkColumn}_idx`;
1762
+ // Check if index already exists in schema definition
1763
+ const existsInSchema = schema.indexes?.some((idx) => idx.name === indexName);
1764
+ try {
1765
+ // Check if index already exists in database
1766
+ const indexExists = await sql `
1767
+ SELECT EXISTS (
1768
+ SELECT FROM pg_indexes
1769
+ WHERE tablename = ${collectionName}
1770
+ AND indexname = ${indexName}
1771
+ )
1772
+ `;
1773
+ if (indexExists[0].exists && existsInSchema) {
1774
+ result.skipped.push({
1775
+ collection: collectionName,
1776
+ indexName,
1777
+ reason: 'Index already exists',
1778
+ });
1779
+ continue;
1780
+ }
1781
+ // Check if column exists
1782
+ const columnExists = await sql `
1783
+ SELECT EXISTS (
1784
+ SELECT FROM information_schema.columns
1785
+ WHERE table_name = ${collectionName}
1786
+ AND column_name = ${fkColumn}
1787
+ )
1788
+ `;
1789
+ if (!columnExists[0].exists) {
1790
+ result.skipped.push({
1791
+ collection: collectionName,
1792
+ indexName,
1793
+ reason: `Column ${fkColumn} does not exist`,
1794
+ });
1795
+ continue;
1796
+ }
1797
+ // Create the index in database if not exists
1798
+ if (!indexExists[0].exists) {
1799
+ const createIndexSQL = `CREATE INDEX "${indexName}" ON "${collectionName}" ("${fkColumn}")`;
1800
+ await sql.unsafe(createIndexSQL);
1801
+ console.log(`Created index ${indexName} on ${collectionName}(${fkColumn})`);
1802
+ }
1803
+ // Add to schema definition if not exists
1804
+ if (!existsInSchema) {
1805
+ newIndexes.push({
1806
+ name: indexName,
1807
+ fields: [fkColumn],
1808
+ unique: false,
1809
+ });
1810
+ }
1811
+ result.created.push({
1812
+ collection: collectionName,
1813
+ indexName,
1814
+ field: fkColumn,
1815
+ });
1816
+ }
1817
+ catch (error) {
1818
+ result.errors.push({
1819
+ collection: collectionName,
1820
+ indexName,
1821
+ error: error.message,
1822
+ });
1823
+ console.error(`Failed to create index ${indexName}:`, error.message);
1824
+ }
1825
+ }
1826
+ }
1827
+ // Check for junction tables (M2M/M2A) and add individual FK indexes
1828
+ if (schema.isJunction) {
1829
+ // Find all FK columns in junction table (columns ending with _id)
1830
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
1831
+ const field = fieldDef;
1832
+ // Skip if field definition is null/undefined
1833
+ if (!field)
1834
+ continue;
1835
+ // Skip non-FK fields and primary keys
1836
+ if (field.primaryKey || field.relType)
1837
+ continue;
1838
+ // Check if it's a FK column (ends with _id or is item_id for M2A)
1839
+ if (fieldName.endsWith('_id') || fieldName === 'item_id') {
1840
+ const indexName = `${collectionName}_${fieldName}_idx`;
1841
+ const existsInSchema = schema.indexes?.some((idx) => idx.name === indexName);
1842
+ try {
1843
+ // Check if index already exists in database
1844
+ const indexExists = await sql `
1845
+ SELECT EXISTS (
1846
+ SELECT FROM pg_indexes
1847
+ WHERE tablename = ${collectionName}
1848
+ AND indexname = ${indexName}
1849
+ )
1850
+ `;
1851
+ if (indexExists[0].exists && existsInSchema) {
1852
+ result.skipped.push({
1853
+ collection: collectionName,
1854
+ indexName,
1855
+ reason: 'Index already exists',
1856
+ });
1857
+ continue;
1858
+ }
1859
+ // Create the index in database if not exists
1860
+ if (!indexExists[0].exists) {
1861
+ const createIndexSQL = `CREATE INDEX "${indexName}" ON "${collectionName}" ("${fieldName}")`;
1862
+ await sql.unsafe(createIndexSQL);
1863
+ console.log(`Created index ${indexName} on ${collectionName}(${fieldName})`);
1864
+ }
1865
+ // Add to schema definition if not exists
1866
+ if (!existsInSchema) {
1867
+ newIndexes.push({
1868
+ name: indexName,
1869
+ fields: [fieldName],
1870
+ unique: false,
1871
+ });
1872
+ }
1873
+ result.created.push({
1874
+ collection: collectionName,
1875
+ indexName,
1876
+ field: fieldName,
1877
+ });
1878
+ }
1879
+ catch (error) {
1880
+ result.errors.push({
1881
+ collection: collectionName,
1882
+ indexName,
1883
+ error: error.message,
1884
+ });
1885
+ console.error(`Failed to create index ${indexName}:`, error.message);
1886
+ }
1887
+ }
1888
+ }
1889
+ // Also add index on 'collection' column for M2A junction tables
1890
+ if (schema.fields.collection) {
1891
+ const indexName = `${collectionName}_collection_idx`;
1892
+ const existsInSchema = schema.indexes?.some((idx) => idx.name === indexName);
1893
+ try {
1894
+ const indexExists = await sql `
1895
+ SELECT EXISTS (
1896
+ SELECT FROM pg_indexes
1897
+ WHERE tablename = ${collectionName}
1898
+ AND indexname = ${indexName}
1899
+ )
1900
+ `;
1901
+ if (indexExists[0].exists && existsInSchema) {
1902
+ result.skipped.push({
1903
+ collection: collectionName,
1904
+ indexName,
1905
+ reason: 'Index already exists',
1906
+ });
1907
+ }
1908
+ else {
1909
+ // Create the index in database if not exists
1910
+ if (!indexExists[0].exists) {
1911
+ const createIndexSQL = `CREATE INDEX "${indexName}" ON "${collectionName}" ("collection")`;
1912
+ await sql.unsafe(createIndexSQL);
1913
+ console.log(`Created index ${indexName} on ${collectionName}(collection)`);
1914
+ }
1915
+ // Add to schema definition if not exists
1916
+ if (!existsInSchema) {
1917
+ newIndexes.push({
1918
+ name: indexName,
1919
+ fields: ['collection'],
1920
+ unique: false,
1921
+ });
1922
+ }
1923
+ result.created.push({
1924
+ collection: collectionName,
1925
+ indexName,
1926
+ field: 'collection',
1927
+ });
1928
+ }
1929
+ }
1930
+ catch (error) {
1931
+ result.errors.push({
1932
+ collection: collectionName,
1933
+ indexName,
1934
+ error: error.message,
1935
+ });
1936
+ }
1937
+ }
1938
+ }
1939
+ // Track schemas that need updating
1940
+ if (newIndexes.length > 0) {
1941
+ schemasToUpdate.set(collectionName, { schema, newIndexes });
1942
+ }
1943
+ }
1944
+ // Update schema definitions with new indexes
1945
+ for (const [collectionName, { schema, newIndexes }] of schemasToUpdate) {
1946
+ try {
1947
+ const updatedSchema = { ...schema };
1948
+ if (!updatedSchema.indexes) {
1949
+ updatedSchema.indexes = [];
1950
+ }
1951
+ updatedSchema.indexes.push(...newIndexes);
1952
+ // Update the schema definition directly in the database
1953
+ await sql `
1954
+ UPDATE "baasix_SchemaDefinition"
1955
+ SET schema = ${JSON.stringify(updatedSchema)}::jsonb,
1956
+ "updatedAt" = NOW()
1957
+ WHERE "collectionName" = ${collectionName}
1958
+ `;
1959
+ console.log(`Updated schema definition for ${collectionName} with ${newIndexes.length} new indexes`);
1960
+ }
1961
+ catch (error) {
1962
+ console.error(`Failed to update schema definition for ${collectionName}:`, error.message);
1963
+ // Don't add to errors since the indexes were created successfully
1964
+ }
1965
+ }
1966
+ console.log(`Index migration complete: ${result.created.length} created, ${result.skipped.length} skipped, ${result.errors.length} errors`);
1967
+ return result;
1968
+ }
1969
+ }
1970
+ /**
1971
+ * Export singleton instance
1972
+ */
1973
+ export const schemaManager = SchemaManager.getInstance();
1974
+ /**
1975
+ * Initialize schema manager
1976
+ */
1977
+ export async function initializeSchemas() {
1978
+ await schemaManager.initialize();
1979
+ }
1980
+ /**
1981
+ * Get schema by collection name
1982
+ */
1983
+ export function getSchema(collectionName) {
1984
+ return schemaManager.getSchema(collectionName);
1985
+ }
1986
+ /**
1987
+ * Get all schemas
1988
+ */
1989
+ export function getAllSchemas() {
1990
+ return schemaManager.getAllSchemas();
1991
+ }
1992
+ //# sourceMappingURL=schemaManager.js.map