@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,1780 @@
1
+ /* eslint-disable no-case-declarations */
2
+ import { APIError } from "../utils/errorHandler.js";
3
+ import { db } from "../utils/db.js";
4
+ import { schemaManager } from "../utils/schemaManager.js";
5
+ import fileUpload from "express-fileupload";
6
+ import permissionService from "../services/PermissionService.js";
7
+ import { invalidateEntireCache } from "../services/CacheService.js";
8
+ import { adminOnly } from "../utils/auth.js";
9
+ import { eq, sql } from "drizzle-orm";
10
+ import { ItemsService } from "../services/ItemsService.js";
11
+ const registerEndpoint = (app, context) => {
12
+ async function validateRelationshipName(name, sourceCollection) {
13
+ /*
14
+ // Get all table names in the database
15
+ const tables = await sequelize.getQueryInterface().showAllTables();
16
+
17
+ // Check if the relationship name matches any table name
18
+ if (tables.includes(name)) {
19
+ throw new APIError(`Relationship name '${name}' cannot be the same as an existing table name`, 400);
20
+ }
21
+ */
22
+ // Check if the relationship name is the same as the source collection
23
+ if (name === sourceCollection) {
24
+ throw new APIError(`Relationship name '${name}' cannot be the same as the collection name ${sourceCollection}`, 400);
25
+ }
26
+ // Check if the name is a reserved word in PostgreSQL
27
+ const reservedWords = [
28
+ "user",
29
+ "group",
30
+ "order",
31
+ "limit",
32
+ "offset",
33
+ "where",
34
+ "select",
35
+ "insert",
36
+ "update",
37
+ "delete",
38
+ "table",
39
+ "from",
40
+ "join",
41
+ "left",
42
+ "right",
43
+ "inner",
44
+ "outer",
45
+ "cross",
46
+ "natural",
47
+ "using",
48
+ "on",
49
+ ];
50
+ if (reservedWords.includes(name.toLowerCase())) {
51
+ throw new APIError(`Relationship name '${name}' cannot be a reserved word`, 400);
52
+ }
53
+ }
54
+ // Helper function to fetch schema definition by collection name
55
+ async function getSchemaDefinition(collectionName, accountability) {
56
+ const schemaService = new ItemsService('baasix_SchemaDefinition', { accountability });
57
+ const result = await schemaService.readByQuery({
58
+ filter: { collectionName },
59
+ limit: 1
60
+ }, true);
61
+ return result.data[0] || null;
62
+ }
63
+ // Get all schemas
64
+ // Access controlled by SCHEMAS_PUBLIC env variable:
65
+ // - true (default): Bypass permission check (for development/CLI)
66
+ // - false: Requires permission on baasix_SchemaDefinition (production)
67
+ // Admins always have access via ItemsService
68
+ app.get("/schemas", async (req, res, next) => {
69
+ try {
70
+ console.log('[schema.route] GET /schemas called');
71
+ const { search, page, limit, sort = "collectionName:asc" } = req.query;
72
+ // Default to public access (backwards compatible), set SCHEMAS_PUBLIC=false for production
73
+ const bypassPermissions = process.env.SCHEMAS_PUBLIC !== 'false';
74
+ // Use ItemsService - bypasses permission if public, otherwise checks permission
75
+ const schemaService = new ItemsService('baasix_SchemaDefinition', {
76
+ accountability: req.accountability
77
+ });
78
+ const query = {
79
+ fields: ['collectionName', 'schema']
80
+ };
81
+ // Only add sort if provided and not the default format that causes parsing issues
82
+ if (sort && typeof sort === 'string' && sort.includes(':')) {
83
+ const [field, direction] = sort.split(':');
84
+ query.sort = [direction?.toLowerCase() === 'desc' ? `-${field}` : field];
85
+ }
86
+ if (search) {
87
+ query.search = search;
88
+ query.searchFields = ['collectionName'];
89
+ }
90
+ if (page !== undefined || limit !== undefined) {
91
+ query.page = parseInt(page || 1, 10);
92
+ query.limit = parseInt(limit || 50, 10);
93
+ }
94
+ const result = await schemaService.readByQuery(query, bypassPermissions);
95
+ // Transform to expected format
96
+ const schemas = result.data.map((item) => ({
97
+ collectionName: item.collectionName,
98
+ schema: item.schema
99
+ }));
100
+ if (page !== undefined || limit !== undefined) {
101
+ const pageNum = parseInt(page || 1, 10);
102
+ const limitNum = parseInt(limit || 50, 10);
103
+ const totalPages = Math.ceil((result.totalCount || 0) / limitNum);
104
+ return res.status(200).json({
105
+ data: schemas,
106
+ totalCount: result.totalCount || schemas.length,
107
+ pagination: {
108
+ currentPage: pageNum,
109
+ totalPages,
110
+ limit: limitNum,
111
+ hasNextPage: pageNum < totalPages,
112
+ hasPrevPage: pageNum > 1,
113
+ nextPage: pageNum < totalPages ? pageNum + 1 : null,
114
+ prevPage: pageNum > 1 ? pageNum - 1 : null
115
+ }
116
+ });
117
+ }
118
+ return res.status(200).json({
119
+ data: schemas,
120
+ totalCount: schemas.length
121
+ });
122
+ }
123
+ catch (error) {
124
+ console.error('[schema.route] Error in GET /schemas:', error);
125
+ next(error);
126
+ }
127
+ });
128
+ // Get a specific schema
129
+ // Access controlled same as GET /schemas
130
+ app.get("/schemas/:collectionName", async (req, res, next) => {
131
+ try {
132
+ // Default to public access (backwards compatible), set SCHEMAS_PUBLIC=false for production
133
+ const bypassPermissions = process.env.SCHEMAS_PUBLIC !== 'false';
134
+ // Use ItemsService - bypasses permission if public, otherwise checks permission
135
+ const schemaService = new ItemsService('baasix_SchemaDefinition', {
136
+ accountability: req.accountability
137
+ });
138
+ const result = await schemaService.readByQuery({
139
+ filter: { collectionName: { eq: req.params.collectionName } },
140
+ limit: 1,
141
+ fields: ['collectionName', 'schema']
142
+ }, bypassPermissions);
143
+ if (!result.data || result.data.length === 0) {
144
+ throw new APIError("Schema not found", 404);
145
+ }
146
+ return res.status(200).json({
147
+ data: {
148
+ collectionName: result.data[0].collectionName,
149
+ schema: result.data[0].schema
150
+ }
151
+ });
152
+ }
153
+ catch (error) {
154
+ next(error);
155
+ }
156
+ });
157
+ // Process schema to handle special flags like usertrack, sortEnabled
158
+ function processSchemaFlags(schema, editMode = false) {
159
+ // Deep clone to avoid mutations
160
+ const processedSchema = JSON.parse(JSON.stringify(schema));
161
+ // Ensure fields object exists
162
+ if (!processedSchema.fields) {
163
+ processedSchema.fields = {};
164
+ }
165
+ // Handle usertrack flag
166
+ if (processedSchema.usertrack === true) {
167
+ const usertrack = {
168
+ userCreated_Id: { type: "UUID", SystemGenerated: true },
169
+ userCreated: {
170
+ relType: "BelongsTo",
171
+ target: "baasix_User",
172
+ foreignKey: "userCreated_Id",
173
+ as: "userCreated",
174
+ SystemGenerated: true,
175
+ description: "M2O",
176
+ },
177
+ userUpdated_Id: { type: "UUID", SystemGenerated: true },
178
+ userUpdated: {
179
+ relType: "BelongsTo",
180
+ target: "baasix_User",
181
+ foreignKey: "userUpdated_Id",
182
+ as: "userUpdated",
183
+ SystemGenerated: true,
184
+ description: "M2O",
185
+ },
186
+ };
187
+ processedSchema.fields = { ...processedSchema.fields, ...usertrack };
188
+ }
189
+ // Handle sortEnabled flag
190
+ if (processedSchema.sortEnabled === true) {
191
+ processedSchema.fields = {
192
+ ...processedSchema.fields,
193
+ sort: {
194
+ type: "Integer",
195
+ allowNull: true,
196
+ description: "Sort order for items",
197
+ SystemGenerated: true
198
+ },
199
+ };
200
+ }
201
+ if (editMode) {
202
+ // If sortEnabled is enabled, ensure the sort field is added
203
+ if (processedSchema.sortEnabled === true) {
204
+ processedSchema.fields.sort = {
205
+ type: "Integer",
206
+ allowNull: true,
207
+ description: "Sort order for items",
208
+ SystemGenerated: true
209
+ };
210
+ }
211
+ // Note: If sortEnabled is disabled, we don't remove the sort field, just keep it
212
+ // If timestamps are enabled, ensure they are not removed
213
+ if (processedSchema.timestamps === true) {
214
+ processedSchema.fields = {
215
+ ...processedSchema.fields,
216
+ createdAt: { type: "DateTime", allowNull: true, SystemGenerated: true, defaultValue: { type: "NOW" } },
217
+ updatedAt: { type: "DateTime", allowNull: true, SystemGenerated: true, defaultValue: { type: "NOW" } },
218
+ };
219
+ }
220
+ else if (processedSchema.timestamps === false) {
221
+ // If timestamps are explicitly disabled, remove them from the schema
222
+ delete processedSchema.fields.createdAt;
223
+ delete processedSchema.fields.updatedAt;
224
+ }
225
+ // If paranoid is enabled, ensure it is not removed
226
+ if (processedSchema.paranoid === true) {
227
+ processedSchema.fields.deletedAt = { type: "DateTime", allowNull: true, SystemGenerated: true, defaultValue: { type: "NOW" } };
228
+ }
229
+ else if (processedSchema.paranoid === false) {
230
+ delete processedSchema.fields.deletedAt;
231
+ }
232
+ }
233
+ return processedSchema;
234
+ }
235
+ // PostgreSQL identifier max length (63 characters)
236
+ const PG_MAX_IDENTIFIER_LENGTH = 63;
237
+ // Validate identifier length for PostgreSQL
238
+ function validateIdentifierLength(name, type = 'Identifier') {
239
+ if (name.length > PG_MAX_IDENTIFIER_LENGTH) {
240
+ throw new APIError(`${type} name too long`, 400, `${type} name "${name}" exceeds PostgreSQL's ${PG_MAX_IDENTIFIER_LENGTH} character limit (${name.length} chars). Please use a shorter name.`);
241
+ }
242
+ }
243
+ app.post("/schemas", adminOnly, async (req, res, next) => {
244
+ try {
245
+ console.log("Creating new schema");
246
+ const { collectionName, schema } = req.body;
247
+ // Process schema flags
248
+ const processedSchema = processSchemaFlags(schema);
249
+ //Return error if collectionName is not provided or ends with _junction
250
+ if (!collectionName || collectionName.endsWith("_junction")) {
251
+ throw new APIError("Invalid collection name", 400, "Collection name cannot be empty or end with _junction");
252
+ }
253
+ // Validate collection name length
254
+ validateIdentifierLength(collectionName, 'Collection');
255
+ // Insert into baasix_SchemaDefinition table
256
+ const schemaDefTable = schemaManager.getTable("baasix_SchemaDefinition");
257
+ await db.insert(schemaDefTable).values({
258
+ collectionName,
259
+ schema: processedSchema,
260
+ });
261
+ // Update in-memory schema (creates Drizzle table)
262
+ await schemaManager.updateModel(collectionName, processedSchema, req.accountability);
263
+ // Invalidate schema definition cache after creating schema
264
+ await invalidateEntireCache('baasix_SchemaDefinition');
265
+ // Sync realtime if the new schema has realtime enabled
266
+ const hasRealtime = processedSchema.realtime === true ||
267
+ (typeof processedSchema.realtime === 'object' && processedSchema.realtime?.enabled);
268
+ if (hasRealtime) {
269
+ try {
270
+ const realtimeService = (await import('../services/RealtimeService.js')).default;
271
+ if (realtimeService.isWalAvailable()) {
272
+ await realtimeService.reloadCollections([collectionName]);
273
+ }
274
+ }
275
+ catch (error) {
276
+ console.warn('Could not sync realtime configuration:', error.message);
277
+ }
278
+ }
279
+ console.log("Schema created successfully");
280
+ res.status(201).json({ message: "Schema created successfully" });
281
+ }
282
+ catch (error) {
283
+ console.error("Error creating schema:", error);
284
+ next(new APIError("Error creating schema", 500, error.message));
285
+ }
286
+ });
287
+ app.patch("/schemas/:collectionName", adminOnly, async (req, res, next) => {
288
+ try {
289
+ console.log(`Updating schema for ${req.params.collectionName}`);
290
+ console.log("New schema:", JSON.stringify(req.body.schema, null, 2));
291
+ const { collectionName } = req.params;
292
+ const { schema } = req.body;
293
+ // Get existing schema to compare flags
294
+ const schemaDefTable = schemaManager.getTable("baasix_SchemaDefinition");
295
+ const existingSchemaRecords = await db
296
+ .select()
297
+ .from(schemaDefTable)
298
+ .where(eq(schemaDefTable.collectionName, collectionName))
299
+ .limit(1);
300
+ if (!existingSchemaRecords || existingSchemaRecords.length === 0) {
301
+ throw new APIError("Schema not found", 404);
302
+ }
303
+ const existingSchema = existingSchemaRecords[0].schema;
304
+ // Check for changes in special flags
305
+ const flagsChanged = existingSchema.usertrack !== schema.usertrack ||
306
+ existingSchema.sortEnabled !== schema.sortEnabled ||
307
+ existingSchema.timestamps !== schema.timestamps ||
308
+ existingSchema.paranoid !== schema.paranoid;
309
+ // Check if realtime config changed
310
+ const realtimeChanged = !deepEqual(existingSchema.realtime, schema.realtime);
311
+ console.log(`Flags changed: ${flagsChanged}`);
312
+ // Process schema flags
313
+ const processedSchema = processSchemaFlags(schema, true);
314
+ // If usertrack was disabled, we don't remove the fields, just keep them
315
+ // If sortEnabled was disabled, we don't remove the sort field, just keep it
316
+ // Update in database
317
+ await db
318
+ .update(schemaDefTable)
319
+ .set({ schema: processedSchema, updatedAt: new Date() })
320
+ .where(eq(schemaDefTable.collectionName, collectionName));
321
+ // Update in-memory schema
322
+ await schemaManager.updateModel(collectionName, processedSchema, req.accountability);
323
+ // Invalidate schema definition cache after updating schema
324
+ await invalidateEntireCache('baasix_SchemaDefinition');
325
+ // Sync realtime if the config changed
326
+ if (realtimeChanged) {
327
+ console.log(`Realtime config changed for ${collectionName}, syncing...`);
328
+ console.log(` Old: ${JSON.stringify(existingSchema.realtime)}`);
329
+ console.log(` New: ${JSON.stringify(schema.realtime)}`);
330
+ try {
331
+ const realtimeService = (await import('../services/RealtimeService.js')).default;
332
+ if (realtimeService.isWalAvailable()) {
333
+ await realtimeService.reloadCollections([collectionName]);
334
+ console.log(`Realtime configuration synced for ${collectionName}`);
335
+ }
336
+ else {
337
+ console.log(`WAL not available, skipping realtime sync for ${collectionName}`);
338
+ }
339
+ }
340
+ catch (error) {
341
+ console.warn('Could not sync realtime configuration:', error.message);
342
+ }
343
+ }
344
+ console.log(`Schema for ${collectionName} updated successfully`);
345
+ res.status(200).json({ message: "Schema updated successfully" });
346
+ }
347
+ catch (error) {
348
+ console.error("Error updating schema:", error);
349
+ next(new APIError("Error updating schema", 500, error.message));
350
+ }
351
+ });
352
+ app.delete("/schemas/:collectionName", adminOnly, async (req, res, next) => {
353
+ try {
354
+ console.log("Deleting schema");
355
+ const { collectionName } = req.params;
356
+ // Delete from database
357
+ const schemaDefTable = schemaManager.getTable("baasix_SchemaDefinition");
358
+ await db
359
+ .delete(schemaDefTable)
360
+ .where(eq(schemaDefTable.collectionName, collectionName));
361
+ // Delete from memory
362
+ await schemaManager.deleteModel(collectionName, req.accountability);
363
+ // Invalidate schema definition cache after deleting schema
364
+ await invalidateEntireCache('baasix_SchemaDefinition');
365
+ // Remove from realtime publication if it was enabled
366
+ try {
367
+ const realtimeService = (await import('../services/RealtimeService.js')).default;
368
+ if (realtimeService.isWalAvailable()) {
369
+ await realtimeService.reloadCollections([collectionName]);
370
+ }
371
+ }
372
+ catch (error) {
373
+ console.warn('Could not sync realtime configuration:', error.message);
374
+ }
375
+ console.log("Schema deleted successfully");
376
+ res.status(200).json({ message: "Schema deleted successfully" });
377
+ }
378
+ catch (error) {
379
+ console.error("Error deleting schema:", error);
380
+ next(new APIError("Error deleting schema", 500, error.message));
381
+ }
382
+ });
383
+ // Add index
384
+ app.post("/schemas/:collectionName/indexes", adminOnly, async (req, res, next) => {
385
+ try {
386
+ const { collectionName } = req.params;
387
+ const indexDefinition = req.body;
388
+ await schemaManager.addIndex(collectionName, indexDefinition, req.accountability);
389
+ // Invalidate schema definition cache after adding index
390
+ await invalidateEntireCache('baasix_SchemaDefinition');
391
+ res.status(201).json({ message: "Index added successfully" });
392
+ }
393
+ catch (error) {
394
+ next(new APIError("Error adding index", 500, error.message));
395
+ }
396
+ });
397
+ // Remove index
398
+ app.delete("/schemas/:collectionName/indexes/:indexName", adminOnly, async (req, res, next) => {
399
+ try {
400
+ const { collectionName, indexName } = req.params;
401
+ await schemaManager.removeIndex(collectionName, indexName, req.accountability);
402
+ // Invalidate schema definition cache after removing index
403
+ await invalidateEntireCache('baasix_SchemaDefinition');
404
+ res.status(200).json({ message: "Index removed successfully" });
405
+ }
406
+ catch (error) {
407
+ next(new APIError("Error removing index", 500, error.message));
408
+ }
409
+ });
410
+ // Add missing foreign key indexes to all collections (migration utility)
411
+ app.post("/schemas/indexes/migrate", adminOnly, async (req, res, next) => {
412
+ try {
413
+ console.log('Starting foreign key index migration...');
414
+ const result = await schemaManager.addMissingForeignKeyIndexes(req.accountability);
415
+ // Invalidate schema definition cache after migration
416
+ await invalidateEntireCache('baasix_SchemaDefinition');
417
+ res.status(200).json({
418
+ message: "Foreign key index migration completed",
419
+ summary: {
420
+ created: result.created.length,
421
+ skipped: result.skipped.length,
422
+ errors: result.errors.length,
423
+ },
424
+ details: result,
425
+ });
426
+ }
427
+ catch (error) {
428
+ next(new APIError("Error migrating indexes", 500, error.message));
429
+ }
430
+ });
431
+ // Create relationship endpoint
432
+ app.post("/schemas/:sourceCollection/relationships", adminOnly, async (req, res, next) => {
433
+ try {
434
+ const { sourceCollection } = req.params;
435
+ const relationshipData = req.body;
436
+ // Validate input
437
+ if (!["M2O", "O2O", "M2M", "M2A", "O2M"].includes(relationshipData.type)) {
438
+ throw new APIError("Invalid relationship type. Must be M2O, O2M, O2O, M2M, or M2A", 400);
439
+ }
440
+ // Validate relationship name
441
+ await validateRelationshipName(relationshipData.name, sourceCollection);
442
+ // If there's an alias, validate it too
443
+ if (relationshipData.alias && relationshipData.target) {
444
+ await validateRelationshipName(relationshipData.alias, relationshipData.target);
445
+ }
446
+ // Get existing schemas
447
+ const sourceSchemaDoc = await getSchemaDefinition(sourceCollection, req.accountability);
448
+ const sourceSchema = sourceSchemaDoc?.schema;
449
+ if (relationshipData.type == "M2A") {
450
+ if (!sourceSchema) {
451
+ throw new APIError("Source or target collection not found", 404);
452
+ }
453
+ // Process the relationship
454
+ const { updatedSourceSchema } = await processRelationship(sourceCollection, sourceSchema, null, relationshipData, req.accountability, false);
455
+ // Apply schema updates
456
+ await schemaManager.updateModel(sourceCollection, updatedSourceSchema, req.accountability);
457
+ }
458
+ else {
459
+ const targetSchemaDoc = await getSchemaDefinition(relationshipData.target, req.accountability);
460
+ const targetSchema = targetSchemaDoc?.schema;
461
+ if (!sourceSchema || !targetSchema) {
462
+ throw new APIError("Source or target collection not found", 404);
463
+ }
464
+ //Check if the relationship is self-referential
465
+ const isSelfReferential = sourceCollection === relationshipData.target;
466
+ // Process the relationship
467
+ const { updatedSourceSchema, updatedTargetSchema } = await processRelationship(sourceCollection, sourceSchema, targetSchema, relationshipData, req.accountability, isSelfReferential);
468
+ // Apply schema updates
469
+ console.log(`[processRelationship] Applying schema updates for ${sourceCollection}`);
470
+ console.log(`[processRelationship] Updated source schema fields:`, Object.keys(updatedSourceSchema.fields));
471
+ await schemaManager.updateModel(sourceCollection, updatedSourceSchema, req.accountability);
472
+ if (updatedTargetSchema && !isSelfReferential) {
473
+ console.log(`[processRelationship] Applying schema updates for ${relationshipData.target}`);
474
+ console.log(`[processRelationship] Updated target schema fields:`, Object.keys(updatedTargetSchema.fields));
475
+ await schemaManager.updateModel(relationshipData.target, updatedTargetSchema, req.accountability);
476
+ }
477
+ }
478
+ // Invalidate schema definition cache after creating relationship
479
+ // This ensures the updated schema with new fields is fetched fresh
480
+ await invalidateEntireCache('baasix_SchemaDefinition');
481
+ res.status(201).json({ message: "Relationship created successfully" });
482
+ }
483
+ catch (error) {
484
+ console.error("Error creating relationship:", error);
485
+ next(new APIError("Error creating relationship", 500, error.message));
486
+ }
487
+ });
488
+ // Update relationship endpoint
489
+ app.patch("/schemas/:sourceCollection/relationships/:fieldName", adminOnly, async (req, res, next) => {
490
+ try {
491
+ const { sourceCollection, fieldName } = req.params;
492
+ const updateData = req.body;
493
+ const sourceSchemaDoc = await getSchemaDefinition(sourceCollection, req.accountability);
494
+ const sourceSchema = sourceSchemaDoc?.schema;
495
+ if (!sourceSchema.fields[fieldName]) {
496
+ throw new APIError("Relationship field not found", 404);
497
+ }
498
+ const updatedField = { ...sourceSchema.fields[fieldName], ...updateData };
499
+ // If it's an M2A relationship and tables are being added or removed
500
+ if (updatedField.relType === "HasMany" && updatedField.polymorphic && updateData.tables) {
501
+ // Pass the original field data (before update) to determine what actually changed
502
+ await updateM2ARelationship(sourceCollection, fieldName, updateData.tables, sourceSchema.fields[fieldName], // Pass original field data
503
+ updatedField, // Pass updated field data for new values
504
+ req.accountability);
505
+ }
506
+ // Update the source schema field AFTER processing M2A changes
507
+ sourceSchema.fields[fieldName] = updatedField;
508
+ await schemaManager.updateModel(sourceCollection, sourceSchema, req.accountability);
509
+ // Update the reverse relationship if it exists
510
+ if (updatedField.target) {
511
+ const targetSchemaDoc = await getSchemaDefinition(updatedField.target, req.accountability);
512
+ const targetSchema = targetSchemaDoc?.schema;
513
+ const reverseField = Object.entries(targetSchema.fields).find(([, field]) => field.target === sourceCollection && field.foreignKey === fieldName);
514
+ if (reverseField) {
515
+ const [reverseFieldName, reverseFieldData] = reverseField;
516
+ targetSchema.fields[reverseFieldName] = {
517
+ ...reverseFieldData,
518
+ onDelete: updateData.onDelete,
519
+ onUpdate: updateData.onUpdate,
520
+ };
521
+ await schemaManager.updateModel(updatedField.target, targetSchema, req.accountability);
522
+ }
523
+ }
524
+ // Invalidate schema definition cache after updating relationship
525
+ await invalidateEntireCache('baasix_SchemaDefinition');
526
+ res.status(200).json({ message: "Relationship updated successfully" });
527
+ }
528
+ catch (error) {
529
+ console.error("Error updating relationship:", error);
530
+ next(new APIError("Error updating relationship", 500, error.message));
531
+ }
532
+ });
533
+ // Delete relationship endpoint
534
+ app.delete("/schemas/:sourceCollection/relationships/:fieldName", adminOnly, async (req, res, next) => {
535
+ try {
536
+ const { sourceCollection, fieldName } = req.params;
537
+ const sourceSchemaDoc = await getSchemaDefinition(sourceCollection, req.accountability);
538
+ const sourceSchema = sourceSchemaDoc?.schema;
539
+ if (!sourceSchema.fields[fieldName]) {
540
+ throw new APIError("Relationship field not found", 404);
541
+ }
542
+ const fieldData = sourceSchema.fields[fieldName];
543
+ //if foreign key exists, delete it
544
+ if (sourceSchema.fields[fieldData.foreignKey] &&
545
+ sourceSchema.fields[fieldData.foreignKey].SystemGenerated) {
546
+ delete sourceSchema.fields[fieldData.foreignKey];
547
+ }
548
+ delete sourceSchema.fields[fieldName];
549
+ await schemaManager.updateModel(sourceCollection, sourceSchema, req.accountability);
550
+ // Remove the reverse relationship if it exists
551
+ if (fieldData.target) {
552
+ const targetSchemaDoc = await getSchemaDefinition(fieldData.target, req.accountability);
553
+ const targetSchema = targetSchemaDoc?.schema;
554
+ const reverseField = Object.entries(targetSchema.fields).find(([, field]) => field.target === sourceCollection && field.foreignKey === fieldData.foreignKey);
555
+ if (reverseField) {
556
+ const [reverseFieldName] = reverseField;
557
+ delete targetSchema.fields[reverseFieldName];
558
+ await schemaManager.updateModel(fieldData.target, targetSchema, req.accountability);
559
+ }
560
+ }
561
+ //If it's an M2M relationship, delete the through table
562
+ if (fieldData.relType === "BelongsToMany" && fieldData.through) {
563
+ //Delete the through table from the database
564
+ await db.execute(sql `DROP TABLE IF EXISTS ${sql.raw(`"${fieldData.through}"`)}`);
565
+ // Delete schema definition
566
+ const schemaToDelete = await getSchemaDefinition(fieldData.through, req.accountability);
567
+ if (schemaToDelete) {
568
+ const schemaDefService = new ItemsService('baasix_SchemaDefinition', { accountability: req.accountability });
569
+ await schemaDefService.deleteOne(schemaToDelete.id);
570
+ }
571
+ }
572
+ // Invalidate schema definition cache after deleting relationship
573
+ await invalidateEntireCache('baasix_SchemaDefinition');
574
+ res.status(200).json({ message: "Relationship deleted successfully" });
575
+ }
576
+ catch (error) {
577
+ next(new APIError("Error deleting relationship", 500, error.message));
578
+ }
579
+ });
580
+ async function processRelationship(sourceCollection, sourceSchema, targetSchema, relationshipData, accountability, isSelfReferential) {
581
+ const updatedSourceSchema = { ...sourceSchema };
582
+ let updatedTargetSchema = { ...targetSchema };
583
+ const { onDelete, onUpdate } = relationshipData;
584
+ console.log("Processing relationship", sourceSchema);
585
+ switch (relationshipData.type) {
586
+ case "O2M":
587
+ updatedSourceSchema.fields[relationshipData.name] = {
588
+ relType: "HasMany",
589
+ target: relationshipData.target,
590
+ foreignKey: relationshipData.foreignKey,
591
+ as: relationshipData.name,
592
+ description: relationshipData.description,
593
+ onDelete,
594
+ onUpdate,
595
+ };
596
+ break;
597
+ case "M2O":
598
+ // Use provided foreignKey or default to name + "_id"
599
+ const m2oForeignKey = relationshipData.foreignKey || (relationshipData.name + "_id");
600
+ updatedSourceSchema.fields[relationshipData.name] = {
601
+ relType: "BelongsTo",
602
+ target: relationshipData.target,
603
+ foreignKey: m2oForeignKey,
604
+ as: relationshipData.name,
605
+ description: relationshipData.description,
606
+ onDelete,
607
+ onUpdate,
608
+ };
609
+ //Add foreign key to source collection, fetching type from target collection schema
610
+ // Find the actual primary key field in the target schema
611
+ const targetPrimaryKeyField = Object.entries(targetSchema.fields).find(([, field]) => field.primaryKey === true);
612
+ const targetPrimaryKeyName = targetPrimaryKeyField ? targetPrimaryKeyField[0] : 'id';
613
+ const targetPrimaryKeyType = targetSchema.fields[targetPrimaryKeyName]?.type || 'UUID';
614
+ updatedSourceSchema.fields[m2oForeignKey] = {
615
+ type: targetPrimaryKeyType,
616
+ allowNull: true,
617
+ SystemGenerated: true,
618
+ };
619
+ // Auto-create index on foreign key for better query performance
620
+ const m2oIndexName = `${sourceCollection}_${m2oForeignKey}_idx`;
621
+ if (!updatedSourceSchema.indexes) {
622
+ updatedSourceSchema.indexes = [];
623
+ }
624
+ // Only add if index doesn't already exist
625
+ if (!updatedSourceSchema.indexes.some((idx) => idx.name === m2oIndexName)) {
626
+ updatedSourceSchema.indexes.push({
627
+ name: m2oIndexName,
628
+ fields: [m2oForeignKey],
629
+ unique: false,
630
+ });
631
+ }
632
+ if (relationshipData.alias) {
633
+ if (isSelfReferential) {
634
+ updatedSourceSchema.fields[relationshipData.alias] = {
635
+ relType: "HasMany",
636
+ target: sourceCollection,
637
+ foreignKey: m2oForeignKey,
638
+ as: relationshipData.alias,
639
+ description: relationshipData.description + " Alias",
640
+ onDelete,
641
+ onUpdate,
642
+ };
643
+ }
644
+ else {
645
+ updatedTargetSchema.fields[relationshipData.alias] = {
646
+ relType: "HasMany",
647
+ target: sourceCollection,
648
+ foreignKey: m2oForeignKey,
649
+ as: relationshipData.alias,
650
+ description: relationshipData.description + " Alias",
651
+ onDelete,
652
+ onUpdate,
653
+ };
654
+ }
655
+ }
656
+ break;
657
+ case "O2O":
658
+ // Use provided foreignKey or default to name + "_id"
659
+ const o2oForeignKey = relationshipData.foreignKey || (relationshipData.name + "_id");
660
+ updatedSourceSchema.fields[relationshipData.name] = {
661
+ relType: "BelongsTo",
662
+ target: relationshipData.target,
663
+ foreignKey: o2oForeignKey,
664
+ as: relationshipData.name,
665
+ description: relationshipData.description,
666
+ onDelete,
667
+ onUpdate,
668
+ };
669
+ //Add foreign key to source collection, fetching type from target collection schema
670
+ // Find the actual primary key field in the target schema
671
+ const o2oTargetPrimaryKeyField = Object.entries(targetSchema.fields).find(([, field]) => field.primaryKey === true);
672
+ const o2oTargetPrimaryKeyName = o2oTargetPrimaryKeyField ? o2oTargetPrimaryKeyField[0] : 'id';
673
+ const o2oTargetPrimaryKeyType = targetSchema.fields[o2oTargetPrimaryKeyName]?.type || 'UUID';
674
+ updatedSourceSchema.fields[o2oForeignKey] = {
675
+ type: o2oTargetPrimaryKeyType,
676
+ allowNull: true,
677
+ SystemGenerated: true,
678
+ };
679
+ // Auto-create index on foreign key for better query performance
680
+ const o2oIndexName = `${sourceCollection}_${o2oForeignKey}_idx`;
681
+ if (!updatedSourceSchema.indexes) {
682
+ updatedSourceSchema.indexes = [];
683
+ }
684
+ // Only add if index doesn't already exist
685
+ if (!updatedSourceSchema.indexes.some((idx) => idx.name === o2oIndexName)) {
686
+ updatedSourceSchema.indexes.push({
687
+ name: o2oIndexName,
688
+ fields: [o2oForeignKey],
689
+ unique: false,
690
+ });
691
+ }
692
+ if (relationshipData.alias) {
693
+ if (isSelfReferential) {
694
+ updatedSourceSchema.fields[relationshipData.alias] = {
695
+ relType: "HasOne",
696
+ target: sourceCollection,
697
+ foreignKey: o2oForeignKey,
698
+ as: relationshipData.alias,
699
+ description: relationshipData.description + " Alias",
700
+ onDelete,
701
+ onUpdate,
702
+ };
703
+ }
704
+ else {
705
+ updatedTargetSchema.fields[relationshipData.alias] = {
706
+ relType: "HasOne",
707
+ target: sourceCollection,
708
+ foreignKey: o2oForeignKey,
709
+ as: relationshipData.alias,
710
+ description: relationshipData.description + " Alias",
711
+ onDelete,
712
+ onUpdate,
713
+ };
714
+ }
715
+ }
716
+ break;
717
+ case "M2M":
718
+ // Use custom junction table name if provided, otherwise generate default
719
+ const through = relationshipData.through || `${sourceCollection}_${relationshipData.target}_${relationshipData.name}_junction`;
720
+ // Validate junction table name length
721
+ validateIdentifierLength(through, 'Junction table');
722
+ const sourceType = sourceSchema.fields.id.type;
723
+ const targetType = targetSchema.fields.id.type;
724
+ // For self-referential M2M relationships, add _2 suffix to avoid column name conflicts
725
+ const sourceIdColumn = `${sourceCollection}_id`;
726
+ const targetIdColumn = isSelfReferential
727
+ ? `${relationshipData.target}_id_2`
728
+ : `${relationshipData.target}_id`;
729
+ const throughSchema = {
730
+ name: through,
731
+ isJunction: true, // Mark this as a junction table for M2M relationships
732
+ fields: {
733
+ id: { type: "Integer", primaryKey: true, defaultValue: { type: "AUTOINCREMENT" } },
734
+ [sourceIdColumn]: { type: sourceType, allowNull: false, SystemGenerated: true },
735
+ [targetIdColumn]: {
736
+ type: targetType,
737
+ allowNull: false,
738
+ SystemGenerated: true,
739
+ },
740
+ [sourceCollection]: {
741
+ relType: "BelongsTo",
742
+ target: sourceCollection,
743
+ foreignKey: sourceIdColumn,
744
+ description: "M2M Junction",
745
+ SystemGenerated: true,
746
+ },
747
+ [isSelfReferential ? `${relationshipData.target}_2` : relationshipData.target]: {
748
+ relType: "BelongsTo",
749
+ target: relationshipData.target,
750
+ foreignKey: targetIdColumn,
751
+ description: "M2M Junction",
752
+ SystemGenerated: true,
753
+ },
754
+ },
755
+ timestamps: true,
756
+ indexes: [
757
+ {
758
+ name: `${sourceCollection}_${relationshipData.target}_unique`,
759
+ fields: [sourceIdColumn, targetIdColumn],
760
+ unique: true,
761
+ },
762
+ // Individual indexes on each FK for better query performance
763
+ {
764
+ name: `${through}_${sourceIdColumn}_idx`,
765
+ fields: [sourceIdColumn],
766
+ unique: false,
767
+ },
768
+ {
769
+ name: `${through}_${targetIdColumn}_idx`,
770
+ fields: [targetIdColumn],
771
+ unique: false,
772
+ },
773
+ ],
774
+ };
775
+ await schemaManager.updateModel(through, throughSchema, accountability);
776
+ // Add HasMany from source to junction
777
+ updatedSourceSchema.fields[relationshipData.name] = {
778
+ relType: "HasMany",
779
+ target: through,
780
+ foreignKey: sourceIdColumn,
781
+ as: relationshipData.name,
782
+ description: relationshipData.description,
783
+ onDelete,
784
+ onUpdate,
785
+ };
786
+ if (relationshipData.alias) {
787
+ if (isSelfReferential) {
788
+ updatedSourceSchema.fields[relationshipData.alias] = {
789
+ relType: "HasMany",
790
+ target: through,
791
+ foreignKey: targetIdColumn,
792
+ as: relationshipData.alias,
793
+ description: relationshipData.description,
794
+ onDelete,
795
+ onUpdate,
796
+ };
797
+ }
798
+ else {
799
+ updatedTargetSchema.fields[relationshipData.alias] = {
800
+ relType: "HasMany",
801
+ target: through,
802
+ foreignKey: targetIdColumn,
803
+ as: relationshipData.alias,
804
+ description: relationshipData.description,
805
+ onDelete,
806
+ onUpdate,
807
+ };
808
+ }
809
+ }
810
+ break;
811
+ case "M2A":
812
+ // Use custom junction table name if provided, otherwise generate default
813
+ const throughTable = relationshipData.through || `${sourceCollection}_${relationshipData.name}_junction`;
814
+ // Validate junction table name length
815
+ validateIdentifierLength(throughTable, 'Junction table');
816
+ //Check type of id in all target tables to ensure they are the same
817
+ const firstTableSchemaDoc = await getSchemaDefinition(relationshipData.tables[0], accountability);
818
+ const firstTableSchema = firstTableSchemaDoc?.schema;
819
+ console.log("First table schema", firstTableSchema);
820
+ if (!firstTableSchema) {
821
+ throw new APIError("Target table not found", 404);
822
+ }
823
+ for (const table of relationshipData.tables) {
824
+ const tableSchemaDoc = await getSchemaDefinition(table, accountability);
825
+ const tableSchema = tableSchemaDoc?.schema;
826
+ if (tableSchema.fields.id.type !== firstTableSchema.fields.id.type) {
827
+ throw new APIError("Target tables must have the same id type", 400);
828
+ }
829
+ }
830
+ // Create through table schema
831
+ const throughSchemaM2A = {
832
+ name: throughTable,
833
+ isJunction: true, // Mark this as a junction table for M2A relationships
834
+ fields: {
835
+ id: { type: "Integer", primaryKey: true, defaultValue: { type: "AUTOINCREMENT" } },
836
+ [`${sourceCollection}_id`]: {
837
+ type: sourceSchema.fields.id.type,
838
+ allowNull: false,
839
+ SystemGenerated: true,
840
+ },
841
+ item_id: {
842
+ type: firstTableSchema.fields.id.type,
843
+ allowNull: false,
844
+ references: null,
845
+ SystemGenerated: true,
846
+ constraints: false,
847
+ },
848
+ collection: {
849
+ type: "String",
850
+ allowNull: false,
851
+ SystemGenerated: true,
852
+ },
853
+ [sourceCollection]: {
854
+ relType: "BelongsTo",
855
+ target: sourceCollection,
856
+ foreignKey: `${sourceCollection}_id`,
857
+ description: "M2A Junction Source",
858
+ SystemGenerated: true,
859
+ },
860
+ },
861
+ indexes: [
862
+ {
863
+ name: `${sourceCollection}_${relationshipData.name}_unique`,
864
+ fields: [`${sourceCollection}_id`, "item_id", "collection"],
865
+ unique: true,
866
+ },
867
+ // Individual indexes on FK columns for better query performance
868
+ {
869
+ name: `${throughTable}_${sourceCollection}_id_idx`,
870
+ fields: [`${sourceCollection}_id`],
871
+ unique: false,
872
+ },
873
+ {
874
+ name: `${throughTable}_item_id_idx`,
875
+ fields: ["item_id"],
876
+ unique: false,
877
+ },
878
+ {
879
+ name: `${throughTable}_collection_idx`,
880
+ fields: ["collection"],
881
+ unique: false,
882
+ },
883
+ ],
884
+ timestamps: true,
885
+ constraints: false,
886
+ };
887
+ for (const table of relationshipData.tables) {
888
+ //Add one to many polymorphic relation from junction table to target tables.
889
+ throughSchemaM2A.fields[table] = {
890
+ relType: "BelongsTo",
891
+ description: "M2A Junction Target",
892
+ target: table,
893
+ foreignKey: "item_id",
894
+ as: table,
895
+ constraints: false,
896
+ SystemGenerated: true,
897
+ };
898
+ }
899
+ // Create through table
900
+ await schemaManager.updateModel(throughTable, throughSchemaM2A, accountability);
901
+ for (const table of relationshipData.tables) {
902
+ // Add HasMany relation from target to junction table
903
+ const targetSchemaDoc = await getSchemaDefinition(table, accountability);
904
+ const targetSchema = targetSchemaDoc?.schema;
905
+ targetSchema.fields[relationshipData.alias] = {
906
+ relType: "HasMany",
907
+ target: throughTable,
908
+ foreignKey: "item_id",
909
+ as: relationshipData.alias,
910
+ description: relationshipData.description,
911
+ constraints: false,
912
+ scope: {
913
+ collection: table.toLowerCase(),
914
+ },
915
+ onDelete,
916
+ onUpdate,
917
+ };
918
+ }
919
+ // Add HasMany relation from source to junction table
920
+ console.log(`[processRelationship] Adding M2A field '${relationshipData.name}' to ${sourceCollection}`);
921
+ updatedSourceSchema.fields[relationshipData.name] = {
922
+ relType: "HasMany",
923
+ target: throughTable,
924
+ foreignKey: `${sourceCollection}_id`,
925
+ as: relationshipData.name,
926
+ description: relationshipData.description,
927
+ tables: relationshipData.tables,
928
+ polymorphic: true,
929
+ onDelete,
930
+ onUpdate,
931
+ };
932
+ console.log(`[processRelationship] Updated source schema fields for ${sourceCollection}:`, Object.keys(updatedSourceSchema.fields));
933
+ break;
934
+ }
935
+ return { updatedSourceSchema, updatedTargetSchema };
936
+ }
937
+ async function updateM2ARelationship(sourceCollection, fieldName, newTables, originalFieldData, updatedFieldData, accountability) {
938
+ console.log("Updating M2A relationship for", sourceCollection, fieldName, newTables);
939
+ const sourceSchemaDoc = await getSchemaDefinition(sourceCollection, accountability);
940
+ const sourceSchema = sourceSchemaDoc?.schema;
941
+ const throughTable = `${sourceCollection}_${fieldName}_junction`;
942
+ // Check type of id in all target tables to ensure they are the same
943
+ const firstTableSchemaDoc = await getSchemaDefinition(newTables[0], accountability);
944
+ const firstTableSchema = firstTableSchemaDoc?.schema;
945
+ if (!firstTableSchema) {
946
+ throw new APIError("Target table not found", 404);
947
+ }
948
+ for (const table of newTables) {
949
+ const tableSchemaDoc = await getSchemaDefinition(table, accountability);
950
+ const tableSchema = tableSchemaDoc?.schema;
951
+ if (tableSchema.fields.id.type !== firstTableSchema.fields.id.type) {
952
+ throw new APIError("Target tables must have the same id type", 400);
953
+ }
954
+ }
955
+ // Get current tables from the existing M2A relationship
956
+ const currentTables = originalFieldData.tables || [];
957
+ console.log(`Current tables in M2A relationship: ${currentTables}`);
958
+ // Determine which tables are being added and removed
959
+ const tablesToAdd = newTables.filter((table) => !currentTables.includes(table));
960
+ const tablesToRemove = currentTables.filter((table) => !newTables.includes(table));
961
+ // Check if there's existing data that would be orphaned by removing tables
962
+ if (tablesToRemove.length > 0) {
963
+ try {
964
+ const junctionService = new ItemsService(throughTable, { accountability });
965
+ for (const table of tablesToRemove) {
966
+ const result = await junctionService.readByQuery({
967
+ filter: { collection: table.toLowerCase() },
968
+ aggregate: { count: { function: 'count', field: 'id' } },
969
+ limit: 0
970
+ }, true);
971
+ const existingCount = result.data?.[0]?.count || 0;
972
+ if (existingCount > 0) {
973
+ throw new APIError(`Cannot remove table '${table}' from M2A relationship because there are ${existingCount} existing records. Please clean up the data first.`, 400);
974
+ }
975
+ }
976
+ }
977
+ catch (error) {
978
+ // If table doesn't exist yet, that's fine - no data to validate
979
+ if (!error.message?.includes('does not exist')) {
980
+ throw error;
981
+ }
982
+ }
983
+ }
984
+ // Also check if there's existing data that would violate new table constraints
985
+ if (tablesToAdd.length > 0) {
986
+ try {
987
+ const junctionService = new ItemsService(throughTable, { accountability });
988
+ // Get all existing data in the junction table
989
+ const existingDataResult = await junctionService.readByQuery({
990
+ fields: ['item_id', 'collection'],
991
+ limit: -1
992
+ }, true);
993
+ const existingData = existingDataResult.data;
994
+ // Check if any existing data references tables that aren't in newTables
995
+ for (const record of existingData) {
996
+ if (!newTables.map((t) => t.toLowerCase()).includes(record.collection)) {
997
+ throw new APIError(`Cannot update M2A relationship because there is existing data referencing table '${record.collection}' which is not in the new table list. Please clean up the data first.`, 400);
998
+ }
999
+ }
1000
+ // Check if existing data has valid references to the new tables being added
1001
+ for (const table of tablesToAdd) {
1002
+ try {
1003
+ const tableService = new ItemsService(table, { accountability });
1004
+ const recordsForTable = existingData.filter((r) => r.collection === table.toLowerCase());
1005
+ for (const record of recordsForTable) {
1006
+ const result = await tableService.readByQuery({
1007
+ filter: { id: record.item_id },
1008
+ aggregate: { count: { function: 'count', field: 'id' } },
1009
+ limit: 0
1010
+ }, true);
1011
+ const exists = result.data?.[0]?.count || 0;
1012
+ if (!exists) {
1013
+ throw new APIError(`Cannot add table '${table}' to M2A relationship because junction table contains item_id=${record.item_id} which doesn't exist in table '${table}'. Please clean up the data first.`, 400);
1014
+ }
1015
+ }
1016
+ }
1017
+ catch (error) {
1018
+ // If target table doesn't exist, skip validation for it
1019
+ if (!error.message?.includes('does not exist')) {
1020
+ throw error;
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ catch (error) {
1026
+ // If junction table doesn't exist yet, that's fine - no data to validate
1027
+ if (!error.message?.includes('does not exist')) {
1028
+ throw error;
1029
+ }
1030
+ }
1031
+ }
1032
+ // Remove relationship from tables no longer in the list
1033
+ for (const table of tablesToRemove) {
1034
+ // Remove the inverse relationship from the target table
1035
+ const targetSchemaDoc = await getSchemaDefinition(table, accountability);
1036
+ const targetSchema = targetSchemaDoc?.schema;
1037
+ if (targetSchema.fields[originalFieldData.alias]) {
1038
+ delete targetSchema.fields[originalFieldData.alias];
1039
+ await schemaManager.updateModel(table, targetSchema, accountability);
1040
+ }
1041
+ }
1042
+ // Get the existing junction table schema to preserve existing structure
1043
+ const existingJunctionSchema = await getSchemaDefinition(throughTable, accountability);
1044
+ if (!existingJunctionSchema) {
1045
+ throw new APIError(`Junction table ${throughTable} not found. Cannot update M2A relationship on non-existent table.`, 404);
1046
+ }
1047
+ console.log(`Updating M2A junction table ${throughTable}: adding ${tablesToAdd.length} tables, removing ${tablesToRemove.length} tables`);
1048
+ // Preserve existing schema completely and only modify table relationships
1049
+ // This ensures any custom fields added to the junction table are preserved
1050
+ const throughSchemaM2A = JSON.parse(JSON.stringify(existingJunctionSchema.schema)); // Deep clone
1051
+ // Ensure item_id field has proper constraints disabled (only if it exists)
1052
+ if (throughSchemaM2A.fields.item_id) {
1053
+ throughSchemaM2A.fields.item_id = {
1054
+ ...throughSchemaM2A.fields.item_id,
1055
+ references: null,
1056
+ constraints: false,
1057
+ foreignKey: false,
1058
+ };
1059
+ }
1060
+ // Only remove BelongsTo relationships for tables that are no longer needed
1061
+ // Preserve any custom fields that are not table relationships
1062
+ for (const table of tablesToRemove) {
1063
+ if (throughSchemaM2A.fields[table] &&
1064
+ throughSchemaM2A.fields[table].relType === "BelongsTo" &&
1065
+ throughSchemaM2A.fields[table].foreignKey === "item_id") {
1066
+ console.log(`Removing M2A table relationship: ${table}`);
1067
+ delete throughSchemaM2A.fields[table];
1068
+ }
1069
+ }
1070
+ // Add relationships for new tables only
1071
+ for (const table of tablesToAdd) {
1072
+ console.log(`Adding M2A table relationship: ${table}`);
1073
+ throughSchemaM2A.fields[table] = {
1074
+ relType: "BelongsTo",
1075
+ description: "M2A Junction Target",
1076
+ target: table,
1077
+ foreignKey: "item_id",
1078
+ as: table,
1079
+ constraints: false,
1080
+ SystemGenerated: true,
1081
+ };
1082
+ }
1083
+ // Update the through table
1084
+ await schemaManager.updateModel(throughTable, throughSchemaM2A, accountability);
1085
+ // Update the main HasMany relation from source to junction table
1086
+ sourceSchema.fields[fieldName] = {
1087
+ relType: "HasMany",
1088
+ target: throughTable,
1089
+ foreignKey: `${sourceCollection}_id`,
1090
+ as: fieldName,
1091
+ description: updatedFieldData.description || originalFieldData.description,
1092
+ onDelete: updatedFieldData.onDelete || originalFieldData.onDelete,
1093
+ onUpdate: updatedFieldData.onUpdate || originalFieldData.onUpdate,
1094
+ polymorphic: true,
1095
+ tables: newTables,
1096
+ };
1097
+ // Add relationships for newly added tables
1098
+ for (const table of tablesToAdd) {
1099
+ // Add or update inverse relationship in target schema
1100
+ const targetSchemaDoc = await getSchemaDefinition(table, accountability);
1101
+ const targetSchema = targetSchemaDoc?.schema;
1102
+ targetSchema.fields[updatedFieldData.alias || originalFieldData.alias] = {
1103
+ relType: "HasMany",
1104
+ target: throughTable,
1105
+ foreignKey: "item_id",
1106
+ as: updatedFieldData.alias || originalFieldData.alias,
1107
+ description: updatedFieldData.description || originalFieldData.description,
1108
+ constraints: false,
1109
+ scope: {
1110
+ collection: table.toLowerCase(),
1111
+ },
1112
+ onDelete: updatedFieldData.onDelete || originalFieldData.onDelete,
1113
+ onUpdate: updatedFieldData.onUpdate || originalFieldData.onUpdate,
1114
+ };
1115
+ await schemaManager.updateModel(table, targetSchema, accountability);
1116
+ }
1117
+ // Update existing tables in case the alias or other properties changed
1118
+ for (const table of newTables.filter(t => !tablesToAdd.includes(t))) {
1119
+ const targetSchemaDoc = await getSchemaDefinition(table, accountability);
1120
+ const targetSchema = targetSchemaDoc?.schema;
1121
+ if (targetSchema.fields[originalFieldData.alias]) {
1122
+ targetSchema.fields[updatedFieldData.alias || originalFieldData.alias] = {
1123
+ relType: "HasMany",
1124
+ target: throughTable,
1125
+ foreignKey: "item_id",
1126
+ as: updatedFieldData.alias || originalFieldData.alias,
1127
+ description: updatedFieldData.description || originalFieldData.description,
1128
+ constraints: false,
1129
+ scope: {
1130
+ collection: table.toLowerCase(),
1131
+ },
1132
+ onDelete: updatedFieldData.onDelete || originalFieldData.onDelete,
1133
+ onUpdate: updatedFieldData.onUpdate || originalFieldData.onUpdate,
1134
+ };
1135
+ await schemaManager.updateModel(table, targetSchema, accountability);
1136
+ }
1137
+ }
1138
+ // Update the source schema
1139
+ await schemaManager.updateModel(sourceCollection, sourceSchema, accountability);
1140
+ }
1141
+ // Export schema as JSON file
1142
+ app.get("/schemas-export", adminOnly, async (req, res, next) => {
1143
+ try {
1144
+ const schemaService = new ItemsService('baasix_SchemaDefinition', { accountability: req.accountability });
1145
+ const schemasResult = await schemaService.readByQuery({
1146
+ sort: ['collectionName'],
1147
+ limit: -1
1148
+ }, true);
1149
+ const schemas = schemasResult.data;
1150
+ // Create a versioned export with metadata
1151
+ const schemaExport = {
1152
+ version: "1.0",
1153
+ timestamp: new Date().toISOString(),
1154
+ schemas: schemas.map((schema) => ({
1155
+ collectionName: schema.collectionName,
1156
+ schema: schema.schema,
1157
+ createdAt: schema.createdAt,
1158
+ updatedAt: schema.updatedAt,
1159
+ })),
1160
+ };
1161
+ // Set headers for file download
1162
+ res.setHeader("Content-Type", "application/json");
1163
+ res.setHeader("Content-Disposition", `attachment; filename=schema-export-${Date.now()}.json`);
1164
+ // Send the JSON as a file download response to the client browser as buffer
1165
+ res.status(200).send(Buffer.from(JSON.stringify(schemaExport, null, 2)));
1166
+ }
1167
+ catch (error) {
1168
+ next(new APIError("Error exporting schemas", 500, error.message));
1169
+ }
1170
+ });
1171
+ // Preview schema changes from uploaded file
1172
+ app.post("/schemas-preview-import", adminOnly, fileUpload({ limits: { fileSize: 50 * 1024 * 1024 } }), // 50MB limit
1173
+ async (req, res, next) => {
1174
+ try {
1175
+ if (!req.files || !req.files.schema) {
1176
+ throw new APIError("No schema file uploaded", 400);
1177
+ }
1178
+ const schemaFile = req.files.schema;
1179
+ // Validate file type
1180
+ if (!schemaFile.mimetype.includes("application/json")) {
1181
+ throw new APIError("Invalid file type. Please upload a JSON file", 400);
1182
+ }
1183
+ // Parse the uploaded JSON file
1184
+ let importData;
1185
+ try {
1186
+ importData = JSON.parse(schemaFile.data.toString());
1187
+ }
1188
+ catch (error) {
1189
+ throw new APIError("Invalid JSON file", 400);
1190
+ }
1191
+ const schemaService = new ItemsService('baasix_SchemaDefinition', { accountability: req.accountability });
1192
+ const currentSchemasResult = await schemaService.readByQuery({ limit: -1 }, true);
1193
+ const currentSchemas = currentSchemasResult.data;
1194
+ const currentSchemaMap = new Map(currentSchemas.map((s) => [s.collectionName, s]));
1195
+ // Analyze changes
1196
+ const changes = {
1197
+ new: [],
1198
+ modified: [],
1199
+ deleted: [],
1200
+ unchanged: [],
1201
+ };
1202
+ // Check for new and modified schemas
1203
+ for (const importSchema of importData.schemas) {
1204
+ const currentSchema = currentSchemaMap.get(importSchema.collectionName);
1205
+ if (!currentSchema) {
1206
+ changes.new.push({
1207
+ collectionName: importSchema.collectionName,
1208
+ details: "New schema will be created",
1209
+ });
1210
+ continue;
1211
+ }
1212
+ const differences = compareSchemas(currentSchema.schema, importSchema.schema);
1213
+ if (Object.keys(differences).length > 0) {
1214
+ changes.modified.push({
1215
+ collectionName: importSchema.collectionName,
1216
+ differences,
1217
+ });
1218
+ }
1219
+ else {
1220
+ changes.unchanged.push(importSchema.collectionName);
1221
+ }
1222
+ }
1223
+ // Check for deleted schemas
1224
+ for (const [collectionName, schema] of currentSchemaMap) {
1225
+ if (!importData.schemas.find((s) => s.collectionName === collectionName)) {
1226
+ changes.deleted.push(collectionName);
1227
+ }
1228
+ }
1229
+ res.status(200).json({
1230
+ importVersion: importData.version,
1231
+ importTimestamp: importData.timestamp,
1232
+ changes,
1233
+ });
1234
+ }
1235
+ catch (error) {
1236
+ next(new APIError("Error analyzing schema changes", 500, error.message));
1237
+ }
1238
+ });
1239
+ // Import schema from uploaded file
1240
+ app.post("/schemas-import", adminOnly, fileUpload({ limits: { fileSize: 50 * 1024 * 1024 } }), // 50MB limit
1241
+ async (req, res, next) => {
1242
+ try {
1243
+ if (!req.files || !req.files.schema) {
1244
+ throw new APIError("No schema file uploaded", 400);
1245
+ }
1246
+ const schemaFile = req.files.schema;
1247
+ // Validate file type
1248
+ if (!schemaFile.mimetype.includes("application/json")) {
1249
+ throw new APIError("Invalid file type. Please upload a JSON file", 400);
1250
+ }
1251
+ // Parse the uploaded JSON file
1252
+ let importData;
1253
+ try {
1254
+ importData = JSON.parse(schemaFile.data.toString());
1255
+ }
1256
+ catch (error) {
1257
+ throw new APIError("Invalid JSON file", 400);
1258
+ }
1259
+ // Validate import data structure
1260
+ if (!importData.version || !importData.schemas) {
1261
+ throw new APIError("Invalid import data format", 400);
1262
+ }
1263
+ // Track all changes made during import
1264
+ const changes = {
1265
+ created: [],
1266
+ updated: [],
1267
+ unchanged: [],
1268
+ deleted: [],
1269
+ errors: [],
1270
+ };
1271
+ // Track collections with realtime config changes for efficient sync
1272
+ const realtimeChangedCollections = [];
1273
+ // Process each schema
1274
+ for (const schemaData of importData.schemas) {
1275
+ try {
1276
+ const existingSchema = await getSchemaDefinition(schemaData.collectionName, req.accountability);
1277
+ // Process schema flags
1278
+ const processedSchema = processSchemaFlags(schemaData.schema);
1279
+ if (existingSchema) {
1280
+ // Check if schema has actually changed
1281
+ const differences = compareSchemas(existingSchema.schema, processedSchema);
1282
+ if (Object.keys(differences).length > 0) {
1283
+ // Track if realtime config changed
1284
+ if (differences.realtime) {
1285
+ realtimeChangedCollections.push(schemaData.collectionName);
1286
+ }
1287
+ // Update existing schema only if there are changes
1288
+ await schemaManager.updateModel(schemaData.collectionName, processedSchema, req.accountability);
1289
+ changes.updated.push(schemaData.collectionName);
1290
+ }
1291
+ else {
1292
+ // Schema is unchanged, skip syncing
1293
+ console.log(`Schema ${schemaData.collectionName} is unchanged, skipping sync`);
1294
+ changes.unchanged.push(schemaData.collectionName);
1295
+ }
1296
+ }
1297
+ else {
1298
+ // Create new schema - check if it has realtime enabled
1299
+ if (processedSchema.realtime === true ||
1300
+ (typeof processedSchema.realtime === 'object' && processedSchema.realtime?.enabled)) {
1301
+ realtimeChangedCollections.push(schemaData.collectionName);
1302
+ }
1303
+ // Create new schema
1304
+ await schemaManager.updateModel(schemaData.collectionName, processedSchema, req.accountability);
1305
+ changes.created.push(schemaData.collectionName);
1306
+ }
1307
+ }
1308
+ catch (error) {
1309
+ console.error(`Error importing schema ${schemaData.collectionName}:`, error);
1310
+ changes.errors.push({
1311
+ collectionName: schemaData.collectionName,
1312
+ error: error.message,
1313
+ });
1314
+ throw new APIError("Schema import failed", 400, changes);
1315
+ }
1316
+ }
1317
+ await invalidateEntireCache();
1318
+ // Reload realtime configuration only for collections with realtime changes
1319
+ if (realtimeChangedCollections.length > 0) {
1320
+ try {
1321
+ const realtimeService = (await import('../services/RealtimeService.js')).default;
1322
+ if (realtimeService.isWalAvailable()) {
1323
+ await realtimeService.reloadCollections(realtimeChangedCollections);
1324
+ console.log(`Realtime configuration reloaded for ${realtimeChangedCollections.length} collections: ${realtimeChangedCollections.join(', ')}`);
1325
+ }
1326
+ }
1327
+ catch (error) {
1328
+ console.warn('Could not reload realtime configuration:', error.message);
1329
+ }
1330
+ }
1331
+ res.status(200).json({
1332
+ message: "Schema import completed",
1333
+ changes,
1334
+ });
1335
+ }
1336
+ catch (error) {
1337
+ throw new APIError("Schema import failed", 400, error);
1338
+ }
1339
+ });
1340
+ // Helper function for deep equality comparison (ignores property order)
1341
+ function deepEqual(obj1, obj2) {
1342
+ if (obj1 === obj2)
1343
+ return true;
1344
+ if (obj1 === null || obj2 === null)
1345
+ return false;
1346
+ if (typeof obj1 !== typeof obj2)
1347
+ return false;
1348
+ if (typeof obj1 !== 'object')
1349
+ return obj1 === obj2;
1350
+ if (Array.isArray(obj1) !== Array.isArray(obj2))
1351
+ return false;
1352
+ if (Array.isArray(obj1)) {
1353
+ if (obj1.length !== obj2.length)
1354
+ return false;
1355
+ for (let i = 0; i < obj1.length; i++) {
1356
+ if (!deepEqual(obj1[i], obj2[i]))
1357
+ return false;
1358
+ }
1359
+ return true;
1360
+ }
1361
+ const keys1 = Object.keys(obj1);
1362
+ const keys2 = Object.keys(obj2);
1363
+ if (keys1.length !== keys2.length)
1364
+ return false;
1365
+ for (const key of keys1) {
1366
+ if (!keys2.includes(key))
1367
+ return false;
1368
+ if (!deepEqual(obj1[key], obj2[key]))
1369
+ return false;
1370
+ }
1371
+ return true;
1372
+ }
1373
+ // Helper function to normalize schema for comparison
1374
+ // Removes SystemGenerated field as it's metadata that doesn't affect DB structure
1375
+ function normalizeSchemaForComparison(schema) {
1376
+ if (!schema)
1377
+ return schema;
1378
+ const normalized = { ...schema };
1379
+ if (normalized.fields) {
1380
+ normalized.fields = { ...normalized.fields };
1381
+ for (const fieldName of Object.keys(normalized.fields)) {
1382
+ const field = normalized.fields[fieldName];
1383
+ if (field && typeof field === 'object') {
1384
+ // Create a copy without SystemGenerated for comparison
1385
+ const { SystemGenerated, ...fieldWithoutSystemGenerated } = field;
1386
+ normalized.fields[fieldName] = fieldWithoutSystemGenerated;
1387
+ }
1388
+ }
1389
+ }
1390
+ return normalized;
1391
+ }
1392
+ // Helper function to compare schemas
1393
+ function compareSchemas(currentSchema, newSchema) {
1394
+ // Normalize both schemas before comparison
1395
+ const normalizedCurrent = normalizeSchemaForComparison(currentSchema);
1396
+ const normalizedNew = normalizeSchemaForComparison(newSchema);
1397
+ const differences = {};
1398
+ // Compare fields
1399
+ const allFields = new Set([...Object.keys(normalizedCurrent.fields || {}), ...Object.keys(normalizedNew.fields || {})]);
1400
+ for (const field of allFields) {
1401
+ if (!normalizedCurrent.fields[field]) {
1402
+ differences[field] = {
1403
+ type: "added",
1404
+ details: normalizedNew.fields[field],
1405
+ };
1406
+ }
1407
+ else if (!normalizedNew.fields[field]) {
1408
+ differences[field] = {
1409
+ type: "removed",
1410
+ details: normalizedCurrent.fields[field],
1411
+ };
1412
+ }
1413
+ else if (!deepEqual(normalizedCurrent.fields[field], normalizedNew.fields[field])) {
1414
+ differences[field] = {
1415
+ type: "modified",
1416
+ from: normalizedCurrent.fields[field],
1417
+ to: normalizedNew.fields[field],
1418
+ };
1419
+ }
1420
+ }
1421
+ // Compare indexes
1422
+ if (normalizedCurrent.indexes || normalizedNew.indexes) {
1423
+ const currentIndexes = normalizedCurrent.indexes || [];
1424
+ const newIndexes = normalizedNew.indexes || [];
1425
+ if (!deepEqual(currentIndexes, newIndexes)) {
1426
+ differences.indexes = {
1427
+ type: "modified",
1428
+ from: currentIndexes,
1429
+ to: newIndexes,
1430
+ };
1431
+ }
1432
+ }
1433
+ // Compare realtime configuration
1434
+ if (normalizedCurrent.realtime !== undefined || normalizedNew.realtime !== undefined) {
1435
+ if (!deepEqual(normalizedCurrent.realtime, normalizedNew.realtime)) {
1436
+ differences.realtime = {
1437
+ type: "modified",
1438
+ from: normalizedCurrent.realtime,
1439
+ to: normalizedNew.realtime,
1440
+ };
1441
+ }
1442
+ }
1443
+ // Compare other properties
1444
+ const schemaProps = ["timestamps", "paranoid", "name"];
1445
+ for (const prop of schemaProps) {
1446
+ if (normalizedCurrent[prop] !== normalizedNew[prop]) {
1447
+ differences[prop] = {
1448
+ type: "modified",
1449
+ from: normalizedCurrent[prop],
1450
+ to: normalizedNew[prop],
1451
+ };
1452
+ }
1453
+ }
1454
+ return differences;
1455
+ }
1456
+ // Add these endpoints to the permission.route.js file
1457
+ // Export roles and permissions
1458
+ app.get("/permissions-export", adminOnly, async (req, res, next) => {
1459
+ try {
1460
+ const roleService = new ItemsService('baasix_Role', { accountability: req.accountability });
1461
+ const permissionItemsService = new ItemsService('baasix_Permission', { accountability: req.accountability });
1462
+ // Get all roles sorted by name
1463
+ const rolesResult = await roleService.readByQuery({
1464
+ sort: ['name'],
1465
+ limit: -1
1466
+ }, true);
1467
+ const roles = rolesResult.data;
1468
+ // Get all permissions sorted by role_Id, collection, and action
1469
+ const permissionsResult = await permissionItemsService.readByQuery({
1470
+ sort: ['role_Id', 'collection', 'action'],
1471
+ limit: -1
1472
+ }, true);
1473
+ const permissions = permissionsResult.data;
1474
+ // Group permissions by role_Id
1475
+ const permissionsByRole = permissions.reduce((acc, permission) => {
1476
+ if (!acc[permission.role_Id]) {
1477
+ acc[permission.role_Id] = [];
1478
+ }
1479
+ acc[permission.role_Id].push(permission);
1480
+ return acc;
1481
+ }, {});
1482
+ // Create a versioned export with metadata
1483
+ const exportData = {
1484
+ version: "1.0",
1485
+ timestamp: new Date().toISOString(),
1486
+ roles: roles.map((role) => ({
1487
+ name: role.name,
1488
+ description: role.description,
1489
+ permissions: (permissionsByRole[role.id] || []).map((permission) => ({
1490
+ collection: permission.collection,
1491
+ action: permission.action,
1492
+ fields: permission.fields,
1493
+ conditions: permission.conditions,
1494
+ defaultValues: permission.defaultValues,
1495
+ relConditions: permission.relConditions,
1496
+ })),
1497
+ })),
1498
+ };
1499
+ // Set headers for file download
1500
+ res.setHeader("Content-Type", "application/json");
1501
+ res.setHeader("Content-Disposition", `attachment; filename=roles-permissions-export-${Date.now()}.json`);
1502
+ res.status(200).send(Buffer.from(JSON.stringify(exportData, null, 2)));
1503
+ }
1504
+ catch (error) {
1505
+ next(new APIError("Error exporting roles and permissions", 500, error.message));
1506
+ }
1507
+ });
1508
+ // Preview roles and permissions import
1509
+ app.post("/permissions-preview-import", adminOnly, fileUpload({ limits: { fileSize: 50 * 1024 * 1024 } }), async (req, res, next) => {
1510
+ try {
1511
+ if (!req.files || !req.files.rolesPermissions) {
1512
+ throw new APIError("No roles & permissions file uploaded", 400);
1513
+ }
1514
+ const uploadedFile = req.files.rolesPermissions;
1515
+ if (!uploadedFile.mimetype.includes("application/json")) {
1516
+ throw new APIError("Invalid file type. Please upload a JSON file", 400);
1517
+ }
1518
+ let importData;
1519
+ try {
1520
+ importData = JSON.parse(uploadedFile.data.toString());
1521
+ }
1522
+ catch (error) {
1523
+ throw new APIError("Invalid JSON file", 400);
1524
+ }
1525
+ const roleService = new ItemsService('baasix_Role', { accountability: req.accountability });
1526
+ const permissionItemsService = new ItemsService('baasix_Permission', { accountability: req.accountability });
1527
+ const currentRolesResult = await roleService.readByQuery({ limit: -1 }, true);
1528
+ const currentRoles = currentRolesResult.data;
1529
+ const permissionsResult = await permissionItemsService.readByQuery({ limit: -1 }, true);
1530
+ const permissions = permissionsResult.data;
1531
+ // Group permissions by role_Id
1532
+ const permissionsByRole = permissions.reduce((acc, permission) => {
1533
+ if (!acc[permission.role_Id]) {
1534
+ acc[permission.role_Id] = [];
1535
+ }
1536
+ acc[permission.role_Id].push(permission);
1537
+ return acc;
1538
+ }, {});
1539
+ // Add permissions to roles
1540
+ const currentRolesWithPerms = currentRoles.map((role) => ({
1541
+ ...role,
1542
+ permissions: permissionsByRole[role.id] || []
1543
+ }));
1544
+ const currentRoleMap = new Map(currentRolesWithPerms.map((r) => [r.name, r]));
1545
+ const changes = {
1546
+ new: [],
1547
+ modified: [],
1548
+ deleted: [],
1549
+ unchanged: [],
1550
+ };
1551
+ // Analyze changes for roles and their permissions
1552
+ for (const importRole of importData.roles) {
1553
+ const currentRole = currentRoleMap.get(importRole.name);
1554
+ if (!currentRole) {
1555
+ changes.new.push({
1556
+ name: importRole.name,
1557
+ type: "role",
1558
+ details: `New role with ${importRole.permissions.length} permissions`,
1559
+ });
1560
+ continue;
1561
+ }
1562
+ const differences = compareRoleAndPermissions(currentRole, importRole);
1563
+ if (Object.keys(differences).length > 0) {
1564
+ changes.modified.push({
1565
+ name: importRole.name,
1566
+ type: "role",
1567
+ differences,
1568
+ });
1569
+ }
1570
+ else {
1571
+ changes.unchanged.push(importRole.name);
1572
+ }
1573
+ }
1574
+ // Check for deleted roles
1575
+ for (const [roleName, role] of currentRoleMap) {
1576
+ if (!importData.roles.find((r) => r.name === roleName)) {
1577
+ changes.deleted.push({
1578
+ name: roleName,
1579
+ type: "role",
1580
+ });
1581
+ }
1582
+ }
1583
+ res.status(200).json({
1584
+ importVersion: importData.version,
1585
+ importTimestamp: importData.timestamp,
1586
+ changes,
1587
+ });
1588
+ }
1589
+ catch (error) {
1590
+ console.error("Error analyzing roles and permissions changes:", error);
1591
+ next(new APIError("Error analyzing roles and permissions changes", 500, error.message));
1592
+ }
1593
+ });
1594
+ // Import roles and permissions
1595
+ app.post("/permissions-import", adminOnly, fileUpload({ limits: { fileSize: 50 * 1024 * 1024 } }), async (req, res, next) => {
1596
+ try {
1597
+ if (!req.files || !req.files.rolesPermissions) {
1598
+ throw new APIError("No roles & permissions file uploaded", 400);
1599
+ }
1600
+ const uploadedFile = req.files.rolesPermissions;
1601
+ if (!Array.isArray(uploadedFile) && !uploadedFile.mimetype.includes("application/json")) {
1602
+ throw new APIError("Invalid file type. Please upload a JSON file", 400);
1603
+ }
1604
+ let importData;
1605
+ try {
1606
+ const fileData = Array.isArray(uploadedFile) ? uploadedFile[0].data : uploadedFile.data;
1607
+ importData = JSON.parse(fileData.toString());
1608
+ }
1609
+ catch (error) {
1610
+ throw new APIError("Invalid JSON file", 400);
1611
+ }
1612
+ if (!importData.version || !importData.roles) {
1613
+ throw new APIError("Invalid import data format", 400);
1614
+ }
1615
+ const roleService = new ItemsService('baasix_Role', { accountability: req.accountability });
1616
+ const permissionItemsService = new ItemsService('baasix_Permission', { accountability: req.accountability });
1617
+ const changes = {
1618
+ created: [],
1619
+ updated: [],
1620
+ deleted: [],
1621
+ errors: [],
1622
+ };
1623
+ // Process each role and its permissions
1624
+ for (const roleData of importData.roles) {
1625
+ try {
1626
+ // Check if role exists
1627
+ const existingRolesResult = await roleService.readByQuery({
1628
+ filter: { name: roleData.name },
1629
+ limit: 1
1630
+ }, true);
1631
+ const existingRole = existingRolesResult.data[0];
1632
+ let roleId;
1633
+ if (!existingRole) {
1634
+ // Create new role - createOne returns only ID in Drizzle
1635
+ roleId = await roleService.createOne({
1636
+ name: roleData.name,
1637
+ description: roleData.description
1638
+ });
1639
+ changes.created.push(`Role: ${roleData.name}`);
1640
+ }
1641
+ else {
1642
+ // Update existing role
1643
+ await roleService.updateOne(existingRole.id, {
1644
+ description: roleData.description
1645
+ });
1646
+ roleId = existingRole.id;
1647
+ changes.updated.push(`Role: ${roleData.name}`);
1648
+ }
1649
+ // Delete existing permissions for this role
1650
+ const existingPermsResult = await permissionItemsService.readByQuery({
1651
+ filter: { role_Id: roleId },
1652
+ limit: -1
1653
+ }, true);
1654
+ for (const perm of existingPermsResult.data) {
1655
+ await permissionItemsService.deleteOne(perm.id);
1656
+ }
1657
+ // Create new permissions
1658
+ const permissions = roleData.permissions.map((perm) => ({
1659
+ ...perm,
1660
+ role_Id: roleId,
1661
+ }));
1662
+ for (const perm of permissions) {
1663
+ await permissionItemsService.createOne(perm);
1664
+ }
1665
+ changes.created.push(`Permissions for ${roleData.name}: ${permissions.length}`);
1666
+ }
1667
+ catch (error) {
1668
+ changes.errors.push({
1669
+ role: roleData.name,
1670
+ error: error.message,
1671
+ });
1672
+ }
1673
+ }
1674
+ await invalidateEntireCache();
1675
+ await permissionService.invalidateRoles(); // Reload roles cache
1676
+ await permissionService.loadPermissions(); // Reload permission cache (using imported singleton)
1677
+ res.status(200).json({
1678
+ message: "Roles and permissions import completed",
1679
+ changes,
1680
+ });
1681
+ }
1682
+ catch (error) {
1683
+ next(new APIError("Error importing roles and permissions", 500, error.message));
1684
+ }
1685
+ });
1686
+ // Helper function to compare roles and their permissions
1687
+ function compareRoleAndPermissions(currentRole, importRole) {
1688
+ const differences = {};
1689
+ // Compare basic role properties
1690
+ if (currentRole.description !== importRole.description) {
1691
+ differences.description = {
1692
+ type: "modified",
1693
+ from: currentRole.description,
1694
+ to: importRole.description,
1695
+ };
1696
+ }
1697
+ // Compare permissions
1698
+ const currentPermissions = currentRole.permissions || [];
1699
+ const newPermissions = importRole.permissions || [];
1700
+ // Create maps for easier comparison
1701
+ const currentPermMap = new Map(currentPermissions.map((p) => [
1702
+ `${p.collection}:${p.action}`,
1703
+ {
1704
+ fields: p.fields,
1705
+ conditions: p.conditions,
1706
+ defaultValues: p.defaultValues,
1707
+ relConditions: p.relConditions,
1708
+ },
1709
+ ]));
1710
+ const newPermMap = new Map(newPermissions.map((p) => [
1711
+ `${p.collection}:${p.action}`,
1712
+ {
1713
+ fields: p.fields,
1714
+ conditions: p.conditions,
1715
+ defaultValues: p.defaultValues,
1716
+ relConditions: p.relConditions,
1717
+ },
1718
+ ]));
1719
+ // Check for added and modified permissions
1720
+ for (const [key, newPerm] of newPermMap) {
1721
+ if (!currentPermMap.has(key)) {
1722
+ if (!differences.permissions)
1723
+ differences.permissions = {};
1724
+ if (!differences.permissions.added)
1725
+ differences.permissions.added = [];
1726
+ differences.permissions.added.push(key);
1727
+ }
1728
+ else {
1729
+ const currentPerm = currentPermMap.get(key);
1730
+ if (JSON.stringify(currentPerm) !== JSON.stringify(newPerm)) {
1731
+ if (!differences.permissions)
1732
+ differences.permissions = {};
1733
+ if (!differences.permissions.modified)
1734
+ differences.permissions.modified = [];
1735
+ differences.permissions.modified.push({
1736
+ permission: key,
1737
+ changes: compareObjects(currentPerm, newPerm),
1738
+ });
1739
+ }
1740
+ }
1741
+ }
1742
+ // Check for removed permissions
1743
+ for (const [key] of currentPermMap) {
1744
+ if (!newPermMap.has(key)) {
1745
+ if (!differences.permissions)
1746
+ differences.permissions = {};
1747
+ if (!differences.permissions.removed)
1748
+ differences.permissions.removed = [];
1749
+ differences.permissions.removed.push(key);
1750
+ }
1751
+ }
1752
+ return differences;
1753
+ }
1754
+ // Helper function to compare objects
1755
+ function compareObjects(obj1, obj2) {
1756
+ const changes = {};
1757
+ const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
1758
+ for (const key of allKeys) {
1759
+ if (!Object.prototype.hasOwnProperty.call(obj1, key)) {
1760
+ changes[key] = { type: "added", value: obj2[key] };
1761
+ }
1762
+ else if (!Object.prototype.hasOwnProperty.call(obj2, key)) {
1763
+ changes[key] = { type: "removed", value: obj1[key] };
1764
+ }
1765
+ else if (JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
1766
+ changes[key] = {
1767
+ type: "modified",
1768
+ from: obj1[key],
1769
+ to: obj2[key],
1770
+ };
1771
+ }
1772
+ }
1773
+ return changes;
1774
+ }
1775
+ };
1776
+ export default {
1777
+ id: "schemas",
1778
+ handler: registerEndpoint,
1779
+ };
1780
+ //# sourceMappingURL=schema.route.js.map