@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,2458 @@
1
+ import { and, eq, inArray, sql, asc, desc } from 'drizzle-orm';
2
+ import { alias } from 'drizzle-orm/pg-core';
3
+ import argon2 from 'argon2';
4
+ import { APIError } from '../utils/errorHandler.js';
5
+ import { db, createTransaction, getCacheService } from '../utils/db.js';
6
+ import { schemaManager } from '../utils/schemaManager.js';
7
+ import { hooksManager } from './HooksManager.js';
8
+ import env from '../utils/env.js';
9
+ import { permissionService } from './PermissionService.js';
10
+ import { resolveDynamicVariables } from '../utils/dynamicVariableResolver.js';
11
+ import { drizzleWhere, combineFilters, applyPagination, applyFullTextSearch } from '../utils/queryBuilder.js';
12
+ import { drizzleOrder } from '../utils/orderUtils.js';
13
+ import { expandFieldsWithIncludes, buildSelectWithJoins, loadSeparateRelations, nestJoinedRelations, hasSeparateQueries } from '../utils/relationLoader.js';
14
+ import { processRelationalData, handleHasManyRelationship, handleM2MRelationship, handleM2ARelationship, handleRelatedRecordsBeforeDelete, validateRelationalData, resolveCircularDependencies, processDeferredFields } from '../utils/relationUtils.js';
15
+ import { resolveRelationPath } from '../utils/relationPathResolver.js';
16
+ import fieldUtils from '../utils/fieldUtils.js';
17
+ import valueValidator from '../utils/valueValidator.js';
18
+ import { shouldEnforceTenantContext, validateTenantContext, buildTenantFilter } from '../utils/tenantUtils.js';
19
+ import { buildAggregateAttributes, buildGroupByExpressions } from '../utils/aggregationUtils.js';
20
+ import { softDelete, restore } from '../plugins/softDelete.js';
21
+ /**
22
+ * ItemsService - Core CRUD service for all collections
23
+ *
24
+ * Provides:
25
+ * - Read operations with filters, sorting, pagination, includes
26
+ * - Create/Update/Delete operations with relation handling
27
+ * - Permission enforcement and field-level security
28
+ * - Multi-tenancy support
29
+ * - Lifecycle hooks integration
30
+ * - Soft-delete handling
31
+ */
32
+ export class ItemsService {
33
+ collection;
34
+ accountability;
35
+ tenant;
36
+ table;
37
+ primaryKey;
38
+ isMultiTenant;
39
+ constructor(collection, params = {}) {
40
+ this.collection = collection;
41
+ this.accountability = params.accountability;
42
+ this.tenant = params.tenant;
43
+ // Get table and schema info from schema manager
44
+ this.table = schemaManager.getTable(collection);
45
+ this.primaryKey = schemaManager.getPrimaryKey(collection);
46
+ this.isMultiTenant = env.get('MULTI_TENANT') === 'true';
47
+ // Debug: log table columns
48
+ if (collection === 'products') {
49
+ console.log(`[ItemsService.constructor] Products table columns:`, Object.keys(this.table).filter(k => !k.startsWith('_')));
50
+ }
51
+ }
52
+ /**
53
+ * Get primary key column from table (with type safety)
54
+ */
55
+ getPrimaryKeyColumn() {
56
+ return this.table[this.primaryKey];
57
+ }
58
+ /**
59
+ * Parse ID to correct type (string or number)
60
+ */
61
+ parseId(id) {
62
+ return isNaN(Number(id)) ? id : parseInt(String(id));
63
+ }
64
+ /**
65
+ * Extract all table names involved in a query (including relations)
66
+ * This is CRITICAL for proper cache invalidation
67
+ */
68
+ extractAllTables(includes) {
69
+ const tables = [this.collection]; // Always include main table
70
+ if (!includes || includes.length === 0) {
71
+ return tables;
72
+ }
73
+ for (const include of includes) {
74
+ // Get the relation definition
75
+ const relationDef = schemaManager.getRelation(this.collection, include.relation);
76
+ if (!relationDef) {
77
+ console.warn(`[ItemsService.extractAllTables] Relation not found: ${this.collection}.${include.relation}`);
78
+ continue;
79
+ }
80
+ // Add the related table
81
+ if (relationDef.relatedCollection) {
82
+ tables.push(relationDef.relatedCollection);
83
+ }
84
+ // For M2M relations, add the junction table
85
+ if (relationDef.type === 'M2M' && relationDef.junctionTable) {
86
+ tables.push(relationDef.junctionTable);
87
+ }
88
+ // For M2A (polymorphic) relations, add all possible related collections
89
+ if (relationDef.type === 'M2A' && relationDef.relatedCollections) {
90
+ tables.push(...relationDef.relatedCollections);
91
+ }
92
+ // Recursively handle nested includes
93
+ if (include.include && include.include.length > 0) {
94
+ // Create a temporary service for the related collection to extract its tables
95
+ try {
96
+ const relatedService = new ItemsService(relationDef.relatedCollection, {
97
+ accountability: this.accountability,
98
+ tenant: this.tenant
99
+ });
100
+ const nestedTables = relatedService.extractAllTables(include.include);
101
+ tables.push(...nestedTables);
102
+ }
103
+ catch (error) {
104
+ console.warn(`[ItemsService.extractAllTables] Error extracting nested tables:`, error);
105
+ }
106
+ }
107
+ }
108
+ // Return unique table names only
109
+ return [...new Set(tables)];
110
+ }
111
+ /**
112
+ * Get all involved tables from processed includes
113
+ * Used for cache invalidation tracking
114
+ */
115
+ getInvolvedTables(processedIncludes) {
116
+ const tables = [this.collection]; // Always include main table
117
+ const collectTables = (includes) => {
118
+ for (const include of includes) {
119
+ // Add the related table from the alias
120
+ if (include.alias) {
121
+ const tableName = include.alias.split('_')[0]; // Extract table name from alias
122
+ tables.push(tableName);
123
+ }
124
+ // Also try to get table name from the table object
125
+ if (include.table && typeof include.table === 'object') {
126
+ const tableName = include.table[Symbol.for('drizzle:Name')];
127
+ if (tableName) {
128
+ tables.push(tableName);
129
+ }
130
+ }
131
+ // For M2M relations, also include junction table if available
132
+ if (include.relationType === 'BelongsToMany') {
133
+ // Junction table would be tracked separately in the relation definition
134
+ const relationDef = schemaManager.getRelation(this.collection, include.relation);
135
+ if (relationDef?.junctionTable) {
136
+ tables.push(relationDef.junctionTable);
137
+ }
138
+ }
139
+ // Recursively handle nested includes
140
+ if (include.nested && include.nested.length > 0) {
141
+ collectTables(include.nested);
142
+ }
143
+ }
144
+ };
145
+ collectTables(processedIncludes);
146
+ // Return unique table names only
147
+ return [...new Set(tables)];
148
+ }
149
+ /**
150
+ * Generate cache key from query parameters
151
+ */
152
+ generateCacheKey(query, processedIncludes) {
153
+ const keyParts = [
154
+ `collection:${this.collection}`,
155
+ `filter:${JSON.stringify(query.filter || {})}`,
156
+ `sort:${JSON.stringify(query.sort || {})}`,
157
+ `fields:${JSON.stringify(query.fields || [])}`,
158
+ `limit:${query.limit || 'none'}`,
159
+ `offset:${query.offset || 0}`,
160
+ `page:${query.page || 'none'}`,
161
+ `search:${query.search || ''}`,
162
+ `includes:${processedIncludes.map(i => i.relation).join(',')}`,
163
+ `paranoid:${query.paranoid !== false}`,
164
+ ];
165
+ // Add tenant context if multi-tenant
166
+ if (this.accountability?.tenant) {
167
+ keyParts.push(`tenant:${this.accountability.tenant}`);
168
+ }
169
+ // Add user context for user-specific data
170
+ if (this.accountability?.user?.id) {
171
+ keyParts.push(`user:${this.accountability.user.id}`);
172
+ }
173
+ return keyParts.join('|');
174
+ }
175
+ /**
176
+ * Execute query with cache wrapper
177
+ * Checks cache before querying, stores results after query
178
+ */
179
+ async executeWithCache(cacheKey, tables, queryFn) {
180
+ const cache = getCacheService();
181
+ // If cache is disabled, execute query directly
182
+ if (!cache) {
183
+ return await queryFn();
184
+ }
185
+ try {
186
+ // Try to get from cache
187
+ const cachedResult = await cache.get(cacheKey);
188
+ if (cachedResult !== undefined && cachedResult !== null) {
189
+ console.log(`[Cache] HIT: ${this.collection} - ${cacheKey.substring(0, 100)}...`);
190
+ return cachedResult;
191
+ }
192
+ // Cache miss - execute query
193
+ console.log(`[Cache] MISS: ${this.collection} - ${cacheKey.substring(0, 100)}...`);
194
+ const result = await queryFn();
195
+ // Store in cache
196
+ await cache.put(cacheKey, result, tables);
197
+ console.log(`[Cache] STORED: ${this.collection} - Cached result for tables: ${tables.join(', ')}`);
198
+ return result;
199
+ }
200
+ catch (error) {
201
+ // Cache errors should not break the operation
202
+ console.error('[Cache] Error during cache operation:', error);
203
+ // Fallback to executing query without cache
204
+ return await queryFn();
205
+ }
206
+ }
207
+ /**
208
+ * Invalidate cache for this collection and all related tables
209
+ * Called after any mutation (create, update, delete)
210
+ */
211
+ async invalidateCache(additionalTables = []) {
212
+ const cache = getCacheService();
213
+ if (!cache) {
214
+ // Cache not enabled, nothing to do
215
+ return;
216
+ }
217
+ try {
218
+ // Invalidate by the main collection table
219
+ const tablesToInvalidate = [this.collection, ...additionalTables];
220
+ // Remove duplicates
221
+ const uniqueTables = [...new Set(tablesToInvalidate)];
222
+ console.log(`[ItemsService.invalidateCache] Invalidating cache for tables: ${uniqueTables.join(', ')}`);
223
+ await cache.onMutate({ tables: uniqueTables });
224
+ }
225
+ catch (error) {
226
+ // Cache invalidation failure should not break the operation
227
+ console.error('[ItemsService.invalidateCache] Cache invalidation failed:', error);
228
+ }
229
+ }
230
+ /**
231
+ * Get role ID from accountability
232
+ * The auth middleware should always set role.id from the database
233
+ */
234
+ getRoleId() {
235
+ if (!this.accountability?.role)
236
+ return null;
237
+ // Role should be an object with id set by auth middleware
238
+ if (typeof this.accountability.role === 'object' && this.accountability.role.id) {
239
+ return this.accountability.role.id;
240
+ }
241
+ // Fallback for legacy string role (shouldn't happen with current middleware)
242
+ if (typeof this.accountability.role === 'string') {
243
+ console.warn('[ItemsService.getRoleId] Role is a string, expected object with id. This may cause permission issues.');
244
+ return null;
245
+ }
246
+ return null;
247
+ }
248
+ /**
249
+ * Check if user is administrator
250
+ */
251
+ async isAdministrator() {
252
+ console.log('[isAdministrator] accountability:', JSON.stringify(this.accountability, null, 2));
253
+ if (!this.accountability) {
254
+ console.log('[isAdministrator] No accountability - returning true');
255
+ return true;
256
+ }
257
+ if (Object.keys(this.accountability).length === 0) {
258
+ console.log('[isAdministrator] Empty accountability - returning true');
259
+ return true;
260
+ }
261
+ if (!this.accountability.role) {
262
+ console.log('[isAdministrator] No role - returning false');
263
+ return false;
264
+ }
265
+ // Check user.isAdmin flag first
266
+ console.log('[isAdministrator] Checking user.isAdmin:', this.accountability.user?.isAdmin);
267
+ if (this.accountability.user && this.accountability.user.isAdmin === true) {
268
+ console.log('[isAdministrator] User has isAdmin=true - returning true');
269
+ return true;
270
+ }
271
+ // If role is a string, check directly
272
+ if (typeof this.accountability.role === 'string') {
273
+ console.log('[isAdministrator] Role is string:', this.accountability.role);
274
+ return this.accountability.role === 'administrator';
275
+ }
276
+ // If role is an object with name, check the name
277
+ if (typeof this.accountability.role === 'object' && this.accountability.role.name) {
278
+ console.log('[isAdministrator] Role object name:', this.accountability.role.name);
279
+ return this.accountability.role.name === 'administrator';
280
+ }
281
+ // If role is an object with id, use PermissionService (hybrid cache)
282
+ const roleId = this.accountability.role.id;
283
+ if (roleId) {
284
+ return await permissionService.isAdministratorRoleAsync(roleId);
285
+ }
286
+ return false;
287
+ }
288
+ /**
289
+ * Apply tenant context to query filter
290
+ */
291
+ async enforceTenantContextFilter(filter = {}) {
292
+ if (!this.isMultiTenant)
293
+ return filter;
294
+ const shouldEnforce = await shouldEnforceTenantContext(this);
295
+ if (!shouldEnforce)
296
+ return filter;
297
+ const tenantId = this.tenant || this.accountability?.tenant;
298
+ if (!tenantId) {
299
+ throw new APIError('Tenant context required but not provided', 403);
300
+ }
301
+ // Use buildTenantFilter which handles isPublic bypass for supported collections (e.g., baasix_File)
302
+ const tenantFilter = buildTenantFilter(this.collection, tenantId);
303
+ return combineFilters(filter, tenantFilter);
304
+ }
305
+ /**
306
+ * Validate and enforce tenant context on data
307
+ */
308
+ async validateAndEnforceTenantContext(data) {
309
+ if (!this.isMultiTenant)
310
+ return data;
311
+ const shouldEnforce = await shouldEnforceTenantContext(this);
312
+ if (!shouldEnforce)
313
+ return data;
314
+ const tenantId = this.tenant || this.accountability?.tenant;
315
+ if (!tenantId) {
316
+ throw new APIError('Tenant context required but not provided', 403);
317
+ }
318
+ // Validate tenant context
319
+ await validateTenantContext(data, this);
320
+ // For baasix_User, tenant_Id is not set directly on the user record
321
+ // (users are associated with tenants via userRoles.tenant_Id)
322
+ if (this.collection === 'baasix_User') {
323
+ return data;
324
+ }
325
+ // Enforce tenant_Id
326
+ return {
327
+ ...data,
328
+ tenant_Id: tenantId
329
+ };
330
+ }
331
+ /**
332
+ * Helper to apply nested joins recursively
333
+ */
334
+ applyNestedJoins(baseQuery, includes) {
335
+ for (const include of includes) {
336
+ if (include.separate)
337
+ continue;
338
+ if (include.relationType === 'BelongsTo' || include.relationType === 'HasOne') {
339
+ baseQuery = baseQuery.leftJoin(include.table, include.joinCondition);
340
+ if (include.nested.length > 0) {
341
+ baseQuery = this.applyNestedJoins(baseQuery, include.nested);
342
+ }
343
+ }
344
+ }
345
+ return baseQuery;
346
+ }
347
+ /**
348
+ * Filter relational field attributes based on permission allowed fields
349
+ *
350
+ * When a user has permission to access only specific relational fields like:
351
+ * - members.id
352
+ * - members.fullName
353
+ *
354
+ * And they request "members.*", this method ensures only the allowed fields
355
+ * are included in the query, not all fields from the related table.
356
+ *
357
+ * @param processedIncludes - The expanded includes from expandFieldsWithIncludes
358
+ * @param allowedFields - The allowed fields from permissions (e.g., ['id', 'name', 'members.id', 'members.fullName'])
359
+ * @param parentPath - The current path prefix for nested relations
360
+ */
361
+ filterIncludesByAllowedFields(processedIncludes, allowedFields, parentPath = '') {
362
+ // If no field restrictions or wildcard access, no filtering needed
363
+ if (!allowedFields || allowedFields.includes('*')) {
364
+ return;
365
+ }
366
+ for (const include of processedIncludes) {
367
+ const relationPath = parentPath ? `${parentPath}.${include.relation}` : include.relation;
368
+ // Collect allowed direct fields and nested relations for this relation
369
+ const allowedDirectFields = [];
370
+ const allowedNestedRelations = [];
371
+ let hasAnyFieldForRelation = false;
372
+ for (const field of allowedFields) {
373
+ if (field.startsWith(`${relationPath}.`)) {
374
+ hasAnyFieldForRelation = true;
375
+ // Extract the field name after the relation path
376
+ const remainingPath = field.substring(relationPath.length + 1);
377
+ // If it's a direct field (no more dots), add to direct fields
378
+ if (!remainingPath.includes('.')) {
379
+ if (!allowedDirectFields.includes(remainingPath)) {
380
+ allowedDirectFields.push(remainingPath);
381
+ }
382
+ }
383
+ // If it has more dots, it's a nested relation
384
+ else {
385
+ const nestedRelation = remainingPath.split('.')[0];
386
+ if (!allowedNestedRelations.includes(nestedRelation)) {
387
+ allowedNestedRelations.push(nestedRelation);
388
+ }
389
+ }
390
+ }
391
+ }
392
+ // Only filter attributes if we have DIRECT fields specified for this relation
393
+ // If we only have nested relations, we need all parent attributes to load the relation
394
+ if (hasAnyFieldForRelation && allowedDirectFields.length > 0) {
395
+ // Check if all requested attributes are already in the allowed direct fields
396
+ const allAttributesAllowed = include.attributes.every(attr => allowedDirectFields.includes(attr));
397
+ if (!allAttributesAllowed) {
398
+ // Always ensure primary key is included for proper relation loading
399
+ if (!allowedDirectFields.includes('id')) {
400
+ allowedDirectFields.unshift('id');
401
+ }
402
+ // Filter attributes to only allowed direct fields
403
+ include.attributes = include.attributes.filter(attr => allowedDirectFields.includes(attr));
404
+ // If after filtering we have no attributes, use the allowed direct fields
405
+ if (include.attributes.length === 0) {
406
+ include.attributes = allowedDirectFields;
407
+ }
408
+ }
409
+ }
410
+ // If only nested relations are specified, keep parent attributes intact
411
+ // but ensure id is included for relation loading
412
+ else if (hasAnyFieldForRelation && allowedNestedRelations.length > 0 && allowedDirectFields.length === 0) {
413
+ // Keep all attributes - we need them to load the parent relation
414
+ // Just make sure 'id' is present
415
+ if (!include.attributes.includes('id')) {
416
+ include.attributes.unshift('id');
417
+ }
418
+ }
419
+ // Recursively filter nested includes
420
+ if (include.nested && include.nested.length > 0) {
421
+ this.filterIncludesByAllowedFields(include.nested, allowedFields, relationPath);
422
+ }
423
+ }
424
+ }
425
+ /**
426
+ * Build complete query with filters, sorts, pagination, includes
427
+ */
428
+ async buildQuery(query, options) {
429
+ const { isAdmin, action, bypassPermissions, idFilter } = options;
430
+ // Start with base filter
431
+ let filter = query.filter || {};
432
+ // Apply ID filter if provided
433
+ if (idFilter !== undefined) {
434
+ filter = combineFilters(filter, {
435
+ [this.primaryKey]: idFilter
436
+ });
437
+ }
438
+ // Apply permission filters
439
+ let permissionRelConditions = {};
440
+ if (!bypassPermissions && !isAdmin) {
441
+ const roleId = this.getRoleId();
442
+ // First, check if user has permission to perform this action
443
+ const hasAccess = await permissionService.canAccess(roleId, this.collection, action);
444
+ if (!hasAccess) {
445
+ throw new APIError(`You don't have permission to ${action} items in '${this.collection}'`, 403);
446
+ }
447
+ // Then apply permission filters
448
+ const permissionFilter = await permissionService.getFilter(roleId, this.collection, action, this.accountability);
449
+ if (permissionFilter.conditions) {
450
+ filter = combineFilters(filter, permissionFilter.conditions);
451
+ }
452
+ // Capture relConditions from permissions to apply later
453
+ if (permissionFilter.relConditions && Object.keys(permissionFilter.relConditions).length > 0) {
454
+ permissionRelConditions = permissionFilter.relConditions;
455
+ }
456
+ }
457
+ // Apply tenant context
458
+ filter = await this.enforceTenantContextFilter(filter);
459
+ // Resolve dynamic variables in filter
460
+ filter = await resolveDynamicVariables(filter, this.accountability);
461
+ // Apply soft delete filter (paranoid mode)
462
+ // Exclude soft-deleted records unless paranoid: false is specified in options
463
+ const isParanoid = schemaManager.isParanoid(this.collection);
464
+ const includeDeleted = query.paranoid === false; // Explicit false check
465
+ if (isParanoid && !includeDeleted) {
466
+ // Add deletedAt IS NULL filter
467
+ filter = combineFilters(filter, {
468
+ [`${this.collection}.deletedAt`]: { _is_null: true }
469
+ });
470
+ }
471
+ // Get fields to select
472
+ const fields = query.fields || ['*'];
473
+ // Expand fields with includes
474
+ const { directFields, includes: processedIncludes } = expandFieldsWithIncludes(fields, this.collection);
475
+ // Copy expanded directFields before adding primary key (for sanitization later)
476
+ const expandedDirectFields = [...directFields];
477
+ // Ensure primary key is always included in directFields
478
+ // This is needed for proper record identification and loadHasManyRelations to work correctly
479
+ if (!directFields.includes(this.primaryKey) && !directFields.includes('*')) {
480
+ directFields.unshift(this.primaryKey);
481
+ }
482
+ // Use processed includes from field expansion
483
+ // Note: query.include is not used - includes are derived from fields parameter
484
+ const allIncludes = [...processedIncludes];
485
+ // Apply field-level permission filtering for relational fields
486
+ // This ensures that when a user requests "relation.*", only the fields they have
487
+ // permission to access are included, not all fields from the related table
488
+ if (!bypassPermissions && !isAdmin) {
489
+ const roleId = this.getRoleId();
490
+ if (roleId) {
491
+ const allowedFields = await permissionService.getAllowedFields(roleId, this.collection, action);
492
+ // Filter relational field attributes based on allowed fields
493
+ if (allowedFields && !allowedFields.includes('*')) {
494
+ this.filterIncludesByAllowedFields(allIncludes, allowedFields);
495
+ // Also filter the processedIncludes (which is the same reference used later)
496
+ this.filterIncludesByAllowedFields(processedIncludes, allowedFields);
497
+ }
498
+ }
499
+ }
500
+ // Merge permission relConditions with query relConditions
501
+ // Permission relConditions take precedence (they are security constraints)
502
+ const mergedRelConditions = { ...query.relConditions };
503
+ for (const [relationName, conditions] of Object.entries(permissionRelConditions)) {
504
+ if (mergedRelConditions[relationName]) {
505
+ // Merge conditions for the same relation using combineFilters
506
+ mergedRelConditions[relationName] = combineFilters(mergedRelConditions[relationName], conditions);
507
+ }
508
+ else {
509
+ mergedRelConditions[relationName] = conditions;
510
+ }
511
+ }
512
+ // Apply relConditions to includes (supports nested relConditions)
513
+ if (Object.keys(mergedRelConditions).length > 0) {
514
+ const resolvedRelConditions = await resolveDynamicVariables(mergedRelConditions, this.accountability);
515
+ // Recursive function to apply relConditions to includes and their nested includes
516
+ const applyRelConditionsRecursive = (includes, relConds) => {
517
+ for (const include of includes) {
518
+ const relationConditions = relConds[include.relation];
519
+ if (relationConditions) {
520
+ // Separate filter conditions from nested relation conditions
521
+ const filterConditions = {};
522
+ const nestedRelConditions = {};
523
+ for (const [key, value] of Object.entries(relationConditions)) {
524
+ // Keys that are logical operators (AND, OR) are always filter conditions
525
+ // Everything else is either a field filter or a nested relation name
526
+ if (key === 'AND' || key === 'OR') {
527
+ filterConditions[key] = value;
528
+ }
529
+ else {
530
+ // Check if this key is a nested relation by looking for it in includes
531
+ const isNestedRelation = include.nested.some((nested) => nested.relation === key);
532
+ if (isNestedRelation) {
533
+ // It's a nested relation (e.g., "tasks")
534
+ nestedRelConditions[key] = value;
535
+ }
536
+ else {
537
+ // It's a field filter condition
538
+ filterConditions[key] = value;
539
+ }
540
+ }
541
+ }
542
+ // Apply filter conditions to this include
543
+ if (Object.keys(filterConditions).length > 0) {
544
+ include.where = include.where
545
+ ? combineFilters(include.where, filterConditions)
546
+ : filterConditions;
547
+ }
548
+ // Recursively apply nested relConditions
549
+ if (include.nested && include.nested.length > 0 && Object.keys(nestedRelConditions).length > 0) {
550
+ applyRelConditionsRecursive(include.nested, nestedRelConditions);
551
+ }
552
+ }
553
+ }
554
+ };
555
+ applyRelConditionsRecursive(allIncludes, resolvedRelConditions);
556
+ }
557
+ // Build select columns and joins for relations
558
+ const { selectColumns, joins } = buildSelectWithJoins(this.table, directFields, allIncludes);
559
+ console.log(`[ItemsService.buildQuery] ${this.collection} - directFields:`, directFields, 'selectColumns keys:', Object.keys(selectColumns));
560
+ // Check if we need to add extra joins for sorting by HasMany relation fields
561
+ // This handles the edge case where we sort by a field in a HasMany relation
562
+ if (query.sort && hasSeparateQueries(allIncludes)) {
563
+ // Extract sort fields to check if they reference separate relations
564
+ let sortFields = [];
565
+ if (typeof query.sort === 'string') {
566
+ try {
567
+ const sortObj = JSON.parse(query.sort);
568
+ sortFields = Object.keys(sortObj);
569
+ }
570
+ catch (e) {
571
+ // Ignore parse errors
572
+ }
573
+ }
574
+ else if (Array.isArray(query.sort)) {
575
+ sortFields = query.sort.map(f => f.startsWith('-') ? f.substring(1) : f);
576
+ }
577
+ else {
578
+ sortFields = Object.keys(query.sort);
579
+ }
580
+ // For each sort field that references a relation, add joins
581
+ for (const sortField of sortFields) {
582
+ if (sortField.includes('.')) {
583
+ // It's a relation field - expand it to get includes
584
+ const { includes: sortIncludes } = expandFieldsWithIncludes([sortField], this.collection);
585
+ // Build joins for these includes
586
+ const { joins: extraJoins } = buildSelectWithJoins(this.table, [], sortIncludes);
587
+ // Add these joins if not already present
588
+ joins.push(...extraJoins);
589
+ }
590
+ }
591
+ }
592
+ // Build where clause with join accumulation for relation path filters
593
+ console.log(`[ItemsService.buildQuery] Collection: ${this.collection}, Filter:`, JSON.stringify(filter, null, 2));
594
+ const filterJoins = [];
595
+ let whereClause = drizzleWhere(filter, {
596
+ table: this.table,
597
+ tableName: this.collection,
598
+ schema: this.table, // Pass table columns as schema
599
+ joins: filterJoins // Accumulate joins from relation path filters
600
+ });
601
+ console.log(`[ItemsService.buildQuery] WHERE clause generated:`, whereClause ? 'yes' : 'no (undefined)');
602
+ // Deduplicate filter joins by alias (multiple conditions on same relation create duplicates)
603
+ const uniqueFilterJoins = [];
604
+ const seenAliases = new Set();
605
+ for (const join of filterJoins) {
606
+ if (!seenAliases.has(join.alias)) {
607
+ seenAliases.add(join.alias);
608
+ uniqueFilterJoins.push(join);
609
+ }
610
+ }
611
+ // Replace filterJoins with deduplicated version
612
+ filterJoins.length = 0;
613
+ filterJoins.push(...uniqueFilterJoins);
614
+ // Log filter joins if any were created
615
+ if (filterJoins.length > 0) {
616
+ console.log(`[ItemsService] Filter generated ${filterJoins.length} joins for relation paths`);
617
+ }
618
+ // Apply full-text search if search query is provided
619
+ let searchOrderClause;
620
+ if (query.search) {
621
+ const { searchCondition, orderClause } = applyFullTextSearch(this.collection, this.table, query.search, query.searchFields, query.sortByRelevance);
622
+ // Combine search condition with existing where clause
623
+ if (whereClause) {
624
+ whereClause = and(whereClause, searchCondition);
625
+ }
626
+ else {
627
+ whereClause = searchCondition;
628
+ }
629
+ // Store search order clause for later
630
+ searchOrderClause = orderClause;
631
+ }
632
+ // Build order by clause
633
+ let orderByClause;
634
+ if (query.sort) {
635
+ // Convert sort array to sort object if needed
636
+ let sortObj = null;
637
+ if (Array.isArray(query.sort)) {
638
+ sortObj = query.sort.reduce((acc, field) => {
639
+ if (field.startsWith('-')) {
640
+ acc[field.substring(1)] = 'desc';
641
+ }
642
+ else {
643
+ acc[field] = 'asc';
644
+ }
645
+ return acc;
646
+ }, {});
647
+ }
648
+ else {
649
+ sortObj = query.sort;
650
+ }
651
+ orderByClause = drizzleOrder(sortObj, {
652
+ table: this.table,
653
+ tableName: this.collection
654
+ });
655
+ }
656
+ else if (searchOrderClause) {
657
+ // If no explicit sort but search with relevance, use search order
658
+ orderByClause = [searchOrderClause];
659
+ }
660
+ // Calculate pagination
661
+ const { limit, offset } = applyPagination({
662
+ limit: query.limit,
663
+ page: query.page,
664
+ offset: query.offset
665
+ });
666
+ // Merge filter joins with existing joins
667
+ // Filter joins are from relation path filters (e.g., "userRoles.role.name")
668
+ // Don't convert filterJoins to raw SQL - they will be applied using Drizzle query builder methods
669
+ // use .leftJoin() and .innerJoin() instead of raw SQL
670
+ return {
671
+ whereClause,
672
+ orderByClause,
673
+ selectColumns,
674
+ joins,
675
+ processedIncludes,
676
+ limit,
677
+ offset,
678
+ filterJoins,
679
+ userRequestedFields: expandedDirectFields,
680
+ userRequestedIncludes: processedIncludes
681
+ };
682
+ }
683
+ /**
684
+ * Sanitize records by removing auto-added fields that user didn't request
685
+ * This includes both main collection fields and nested relation fields
686
+ *
687
+ * @param records - Records to sanitize
688
+ * @param userRequestedFields - Expanded direct fields requested by user (before auto-adding primary key)
689
+ * @param userRequestedIncludes - ProcessedIncludes containing relation field info
690
+ */
691
+ sanitizeAutoAddedFields(records, userRequestedFields, userRequestedIncludes) {
692
+ // Build a set of allowed fields for main collection
693
+ const allowedMainFields = new Set(userRequestedFields);
694
+ // Build a map of includes by relation name
695
+ const includesMap = new Map();
696
+ for (const include of userRequestedIncludes) {
697
+ allowedMainFields.add(include.relation); // Allow relation key in main record
698
+ includesMap.set(include.relation, include);
699
+ }
700
+ // Recursive function to sanitize a record
701
+ const sanitizeRecord = (record, allowedFields, includes) => {
702
+ const sanitized = {};
703
+ for (const [key, value] of Object.entries(record)) {
704
+ // Check if this is a relation
705
+ const include = includes.get(key);
706
+ if (include) {
707
+ // This is a relation - recursively sanitize
708
+ const relationAllowedFields = new Set(include.attributes);
709
+ // Build nested includes map
710
+ const nestedIncludesMap = new Map();
711
+ for (const nestedInclude of include.nested || []) {
712
+ relationAllowedFields.add(nestedInclude.relation);
713
+ nestedIncludesMap.set(nestedInclude.relation, nestedInclude);
714
+ }
715
+ if (Array.isArray(value)) {
716
+ sanitized[key] = value.map(item => item && typeof item === 'object'
717
+ ? sanitizeRecord(item, relationAllowedFields, nestedIncludesMap)
718
+ : item);
719
+ }
720
+ else if (value && typeof value === 'object') {
721
+ sanitized[key] = sanitizeRecord(value, relationAllowedFields, nestedIncludesMap);
722
+ }
723
+ else {
724
+ sanitized[key] = value;
725
+ }
726
+ }
727
+ else if (allowedFields.has(key)) {
728
+ // Regular field - keep if explicitly requested
729
+ sanitized[key] = value;
730
+ }
731
+ // else: field not requested, skip it
732
+ }
733
+ return sanitized;
734
+ };
735
+ return records.map(record => sanitizeRecord(record, allowedMainFields, includesMap));
736
+ }
737
+ /**
738
+ * Apply field-level permissions
739
+ */
740
+ async applyFieldPermissions(data, action, isAdmin) {
741
+ if (isAdmin)
742
+ return;
743
+ const roleId = this.getRoleId();
744
+ const allowedFields = await permissionService.getAllowedFields(roleId, this.collection, action);
745
+ if (!allowedFields || allowedFields.length === 0) {
746
+ throw new APIError(`You don't have permission to ${action} this item`, 403);
747
+ }
748
+ // Validate field permissions
749
+ const dataFields = Object.keys(data);
750
+ const relationNames = schemaManager.getRelationNames(this.collection);
751
+ for (const field of dataFields) {
752
+ // Skip relation fields - they'll be handled separately
753
+ if (relationNames.includes(field))
754
+ continue;
755
+ // Check if field is allowed
756
+ if (!allowedFields.includes(field) && !allowedFields.includes('*')) {
757
+ throw new APIError(`You don't have permission to ${action} field: ${field}`, 403);
758
+ }
759
+ }
760
+ }
761
+ /**
762
+ * Get default values from permissions
763
+ */
764
+ async getDefaultValues(action) {
765
+ const roleId = this.getRoleId();
766
+ if (!roleId)
767
+ return {};
768
+ return await permissionService.getDefaultValues(roleId, this.collection, action, this.accountability);
769
+ }
770
+ /**
771
+ * Read with two-query approach for HasMany sorting
772
+ */
773
+ async readWithHasManyHandling(query, whereClause, orderByClause, processedIncludes, limit, offset, isAdmin, bypassPermissions, filterJoins = []) {
774
+ console.log('[ItemsService] Using Drizzle query builder for HasMany sorting/filtering');
775
+ // STEP 1: Build ID query using PostgreSQL DISTINCT ON for proper deduplication
776
+ // DISTINCT ON ensures we get unique IDs BEFORE applying LIMIT, not after
777
+ // This matches Sequelize's approach
778
+ // Build DISTINCT ON clause from sort fields + primary key
779
+ const tableName = this.collection;
780
+ const pkField = `"${tableName}"."${this.primaryKey}"`;
781
+ const distinctOnFields = [];
782
+ // Add sort fields from query.sort to DISTINCT ON
783
+ if (query.sort && Array.isArray(query.sort)) {
784
+ for (const sortItem of query.sort) {
785
+ if (typeof sortItem === 'string') {
786
+ // Simple sort: "fieldName" or "-fieldName"
787
+ const fieldName = sortItem.startsWith('-') ? sortItem.slice(1) : sortItem;
788
+ distinctOnFields.push(`"${tableName}"."${fieldName}"`);
789
+ }
790
+ else if (typeof sortItem === 'object') {
791
+ // Object sort: { fieldName: 'asc' }
792
+ for (const [fieldName, direction] of Object.entries(sortItem)) {
793
+ if (fieldName.includes('.')) {
794
+ // Relation path - need to resolve it
795
+ // For now, skip relations in DISTINCT ON (will fallback to pk only)
796
+ continue;
797
+ }
798
+ distinctOnFields.push(`"${tableName}"."${fieldName}"`);
799
+ }
800
+ }
801
+ }
802
+ }
803
+ // Always add primary key to DISTINCT ON
804
+ if (!distinctOnFields.includes(pkField)) {
805
+ distinctOnFields.push(pkField);
806
+ }
807
+ // Build the DISTINCT ON clause
808
+ const distinctOnClause = distinctOnFields.length > 0
809
+ ? `DISTINCT ON (${distinctOnFields.join(', ')}) ${pkField}`
810
+ : pkField;
811
+ console.log(`[ItemsService] Using DISTINCT ON with fields: ${distinctOnFields.join(', ')}`);
812
+ let idQuery = db
813
+ .select({ [this.primaryKey]: sql.raw(distinctOnClause) })
814
+ .from(this.table)
815
+ .$dynamic();
816
+ // Apply processedIncludes joins using Drizzle query builder
817
+ for (const include of processedIncludes) {
818
+ if (include.separate) {
819
+ // For HasMany relations, we need the join for sorting even though data loads separately
820
+ // Apply the join using Drizzle's leftJoin
821
+ idQuery = idQuery.leftJoin(include.table, include.joinCondition);
822
+ // Apply nested joins recursively
823
+ if (include.nested.length > 0) {
824
+ idQuery = this.applyNestedJoins(idQuery, include.nested);
825
+ }
826
+ }
827
+ else if (include.relationType === 'BelongsTo' || include.relationType === 'HasOne') {
828
+ idQuery = idQuery.leftJoin(include.table, include.joinCondition);
829
+ if (include.nested.length > 0) {
830
+ idQuery = this.applyNestedJoins(idQuery, include.nested);
831
+ }
832
+ }
833
+ }
834
+ // Apply filterJoins if present (using alias for custom aliases)
835
+ if (filterJoins.length > 0) {
836
+ console.log(`[ItemsService] Applying ${filterJoins.length} filterJoins to ID query`);
837
+ for (const filterJoin of filterJoins) {
838
+ const aliasedTable = alias(filterJoin.table, filterJoin.alias);
839
+ const joinMethod = filterJoin.type === 'inner' ? 'innerJoin' :
840
+ filterJoin.type === 'right' ? 'rightJoin' : 'leftJoin';
841
+ idQuery = idQuery[joinMethod](aliasedTable, filterJoin.condition);
842
+ }
843
+ }
844
+ // Apply WHERE clause
845
+ if (whereClause) {
846
+ idQuery = idQuery.where(whereClause);
847
+ }
848
+ // Apply ORDER BY - must match DISTINCT ON for PostgreSQL
849
+ // We need to ensure ORDER BY starts with the same columns as DISTINCT ON
850
+ if (distinctOnFields.length > 0) {
851
+ // Build ORDER BY from DISTINCT ON fields
852
+ const orderByClauses = [];
853
+ // Add all DISTINCT ON fields to ORDER BY (in same order)
854
+ for (const field of distinctOnFields) {
855
+ // Determine direction from original sort if available
856
+ let direction = 'asc';
857
+ if (query.sort && Array.isArray(query.sort)) {
858
+ for (const sortItem of query.sort) {
859
+ if (typeof sortItem === 'object') {
860
+ for (const [fieldName, dir] of Object.entries(sortItem)) {
861
+ if (field.includes(fieldName)) {
862
+ direction = String(dir).toLowerCase();
863
+ break;
864
+ }
865
+ }
866
+ }
867
+ }
868
+ }
869
+ orderByClauses.push(sql.raw(`${field} ${direction.toUpperCase()}`));
870
+ }
871
+ idQuery = idQuery.orderBy(...orderByClauses);
872
+ }
873
+ else if (orderByClause && orderByClause.length > 0) {
874
+ // Fallback to original ORDER BY if no DISTINCT ON
875
+ idQuery = idQuery.orderBy(...orderByClause);
876
+ }
877
+ // Apply pagination
878
+ if (limit !== undefined && limit !== -1) {
879
+ idQuery = idQuery.limit(limit);
880
+ }
881
+ if (offset !== undefined) {
882
+ idQuery = idQuery.offset(offset);
883
+ }
884
+ // Execute ID query
885
+ console.log('[ItemsService] Executing ID query with DISTINCT ON');
886
+ const idRecords = await idQuery;
887
+ // Extract IDs from the result (already deduplicated by DISTINCT ON)
888
+ const ids = idRecords
889
+ .map(record => record[this.primaryKey])
890
+ .filter(id => id != null);
891
+ console.log(`[ItemsService] Got ${ids.length} unique IDs from DISTINCT ON query`);
892
+ // If no IDs found, return empty result
893
+ if (ids.length === 0) {
894
+ return {
895
+ data: [],
896
+ totalCount: 0
897
+ };
898
+ }
899
+ // STEP 2: Get full records for these IDs with original includes
900
+ // Build select columns for full query
901
+ const fields = query.fields || ['*'];
902
+ const { directFields } = expandFieldsWithIncludes(fields, this.collection);
903
+ const { selectColumns } = buildSelectWithJoins(this.table, directFields, processedIncludes // Use original includes with separate: true for HasMany
904
+ );
905
+ let fullQuery = db.select(selectColumns).from(this.table);
906
+ // Apply joins (only BelongsTo/HasOne, not HasMany)
907
+ for (const include of processedIncludes) {
908
+ if (include.separate)
909
+ continue; // Skip HasMany for JOINs
910
+ if (include.relationType === 'BelongsTo' || include.relationType === 'HasOne') {
911
+ fullQuery = fullQuery.leftJoin(include.table, include.joinCondition);
912
+ if (include.nested.length > 0) {
913
+ fullQuery = this.applyNestedJoins(fullQuery, include.nested);
914
+ }
915
+ }
916
+ }
917
+ // Filter by IDs from first query
918
+ fullQuery = fullQuery.where(inArray(this.getPrimaryKeyColumn(), ids));
919
+ // Execute second query
920
+ const records = await fullQuery;
921
+ // Load separate relations (HasMany, BelongsToMany) and nest joined relations
922
+ let finalRecords = records;
923
+ if (hasSeparateQueries(processedIncludes)) {
924
+ // This handles both nesting joined relations and loading separate ones
925
+ finalRecords = await loadSeparateRelations(db, records, processedIncludes, this.collection);
926
+ }
927
+ else {
928
+ // No separate queries, but we still need to nest joined BelongsTo/HasOne relations
929
+ finalRecords = nestJoinedRelations(records, processedIncludes);
930
+ }
931
+ // Maintain order from first query
932
+ const recordMap = new Map(finalRecords.map(r => [r[this.primaryKey], r]));
933
+ const orderedRecords = ids.map(id => recordMap.get(id)).filter(r => r != null);
934
+ // Get total count using Drizzle query builder (same joins as ID query)
935
+ let countQuery = db
936
+ .select({ count: sql `COUNT(DISTINCT ${this.getPrimaryKeyColumn()})`.mapWith(Number) })
937
+ .from(this.table)
938
+ .$dynamic();
939
+ // Apply same joins as ID query
940
+ for (const include of processedIncludes) {
941
+ if (include.separate) {
942
+ countQuery = countQuery.leftJoin(include.table, include.joinCondition);
943
+ if (include.nested.length > 0) {
944
+ countQuery = this.applyNestedJoins(countQuery, include.nested);
945
+ }
946
+ }
947
+ else if (include.relationType === 'BelongsTo' || include.relationType === 'HasOne') {
948
+ countQuery = countQuery.leftJoin(include.table, include.joinCondition);
949
+ if (include.nested.length > 0) {
950
+ countQuery = this.applyNestedJoins(countQuery, include.nested);
951
+ }
952
+ }
953
+ }
954
+ // Apply filterJoins
955
+ if (filterJoins.length > 0) {
956
+ for (const filterJoin of filterJoins) {
957
+ const aliasedTable = alias(filterJoin.table, filterJoin.alias);
958
+ const joinMethod = filterJoin.type === 'inner' ? 'innerJoin' :
959
+ filterJoin.type === 'right' ? 'rightJoin' : 'leftJoin';
960
+ countQuery = countQuery[joinMethod](aliasedTable, filterJoin.condition);
961
+ }
962
+ }
963
+ // Apply WHERE clause
964
+ if (whereClause) {
965
+ countQuery = countQuery.where(whereClause);
966
+ }
967
+ const countResult = await countQuery;
968
+ const totalCount = countResult[0]?.count || 0;
969
+ // Strip hidden fields from records before returning
970
+ const strippedRecords = fieldUtils.stripHiddenFieldsFromRecords(this.collection, orderedRecords);
971
+ return {
972
+ data: strippedRecords,
973
+ totalCount
974
+ };
975
+ }
976
+ /**
977
+ * Check if we need two-query approach for sorting by HasMany relations
978
+ */
979
+ needsHasManyHandling(sortFields, processedIncludes) {
980
+ // Build a map of relation paths to their includes
981
+ const relationMap = new Map();
982
+ const addToMap = (includes, prefix = '') => {
983
+ for (const include of includes) {
984
+ const path = prefix ? `${prefix}.${include.relation}` : include.relation;
985
+ relationMap.set(path, include);
986
+ if (include.nested.length > 0) {
987
+ addToMap(include.nested, path);
988
+ }
989
+ }
990
+ };
991
+ addToMap(processedIncludes);
992
+ // Check if any sort field references a HasMany relation
993
+ for (const sortField of sortFields) {
994
+ if (!sortField.includes('.'))
995
+ continue;
996
+ // Extract relation path (e.g., "userRoles.role.name" -> "userRoles.role")
997
+ const parts = sortField.split('.');
998
+ for (let i = 1; i < parts.length; i++) {
999
+ const relationPath = parts.slice(0, i).join('.');
1000
+ const include = relationMap.get(relationPath);
1001
+ if (include && include.separate) {
1002
+ // This sort field references a HasMany relation
1003
+ return true;
1004
+ }
1005
+ }
1006
+ }
1007
+ return false;
1008
+ }
1009
+ /**
1010
+ * Read records by query
1011
+ */
1012
+ async readByQuery(query = {}, bypassPermissions = false) {
1013
+ // Execute before-read hooks
1014
+ let hookData = await hooksManager.executeHooks(this.collection, 'items.read', this.accountability, { query });
1015
+ const modifiedQuery = hookData.query;
1016
+ try {
1017
+ const isAdmin = await this.isAdministrator();
1018
+ // Check if this is an aggregate query
1019
+ if (modifiedQuery.aggregate) {
1020
+ return await this.executeAggregateQuery(modifiedQuery, isAdmin, bypassPermissions);
1021
+ }
1022
+ // Build query components
1023
+ const { whereClause, orderByClause, selectColumns, joins, processedIncludes, limit, offset, filterJoins = [], userRequestedFields = [], userRequestedIncludes = [] } = await this.buildQuery(modifiedQuery, {
1024
+ isAdmin,
1025
+ action: 'read',
1026
+ bypassPermissions
1027
+ });
1028
+ // Extract sort fields to check if we need two-query approach
1029
+ let sortFields = [];
1030
+ if (modifiedQuery.sort) {
1031
+ if (typeof modifiedQuery.sort === 'string') {
1032
+ try {
1033
+ const sortObj = JSON.parse(modifiedQuery.sort);
1034
+ sortFields = Object.keys(sortObj);
1035
+ }
1036
+ catch (e) {
1037
+ // Ignore parse errors
1038
+ }
1039
+ }
1040
+ else if (Array.isArray(modifiedQuery.sort)) {
1041
+ sortFields = modifiedQuery.sort.map(f => f.startsWith('-') ? f.substring(1) : f);
1042
+ }
1043
+ else {
1044
+ sortFields = Object.keys(modifiedQuery.sort);
1045
+ }
1046
+ }
1047
+ // Check if we need two-query approach for HasMany sorting
1048
+ const needsHasManyHandling = this.needsHasManyHandling(sortFields, processedIncludes);
1049
+ // Check if we have filter joins (relational filters that need joins)
1050
+ const hasFilterJoins = filterJoins && filterJoins.length > 0;
1051
+ // Use readWithHasManyHandling for:
1052
+ // 1. HasMany sorting (to handle duplicates from joins)
1053
+ // 2. HasMany filtering with pagination (to deduplicate before applying LIMIT)
1054
+ // This matches Sequelize's approach of using a two-query pattern
1055
+ if (needsHasManyHandling || hasFilterJoins) {
1056
+ console.log(`[ItemsService] Using readWithHasManyHandling for ${needsHasManyHandling ? 'HasMany sorting' : 'HasMany filtering'}`);
1057
+ return await this.readWithHasManyHandling(modifiedQuery, whereClause, orderByClause, processedIncludes, limit, offset, isAdmin, bypassPermissions, filterJoins);
1058
+ }
1059
+ // Build base query
1060
+ console.log(`[ItemsService.readByQuery] Building base query for ${this.collection}, selectColumns has ${Object.keys(selectColumns).length} keys`);
1061
+ if (Object.keys(selectColumns).length === 0) {
1062
+ console.error(`[ItemsService.readByQuery] ERROR: selectColumns is EMPTY for ${this.collection}! This will cause SQL syntax error.`);
1063
+ console.error(`[ItemsService.readByQuery] query:`, query);
1064
+ }
1065
+ let baseQuery = db.select(selectColumns).from(this.table);
1066
+ // Apply filterJoins using Drizzle's query builder
1067
+ // FilterJoins are created when filtering by relation paths (e.g., "userRoles.role.name")
1068
+ if (hasFilterJoins) {
1069
+ console.log(`[ItemsService] Applying ${filterJoins.length} filterJoins using Drizzle query builder`);
1070
+ for (const filterJoin of filterJoins) {
1071
+ // Use alias() to create an aliased table with the exact alias used in the WHERE clause
1072
+ const aliasedTable = alias(filterJoin.table, filterJoin.alias);
1073
+ // Apply join using Drizzle's leftJoin (or other join type)
1074
+ const joinMethod = filterJoin.type === 'inner' ? 'innerJoin' :
1075
+ filterJoin.type === 'right' ? 'rightJoin' : 'leftJoin';
1076
+ baseQuery = baseQuery[joinMethod](aliasedTable, filterJoin.condition);
1077
+ console.log(`[ItemsService] Applied ${joinMethod}: ${filterJoin.tableName} AS ${filterJoin.alias}`);
1078
+ }
1079
+ }
1080
+ // Apply processedIncludes joins (for fetching related data in SELECT)
1081
+ if (processedIncludes.length > 0) {
1082
+ for (const include of processedIncludes) {
1083
+ if (include.separate)
1084
+ continue; // Skip HasMany relations loaded separately
1085
+ if (include.relationType === 'BelongsTo' || include.relationType === 'HasOne') {
1086
+ // Apply join with Drizzle API
1087
+ baseQuery = baseQuery.leftJoin(include.table, include.joinCondition);
1088
+ // Apply nested joins recursively
1089
+ if (include.nested.length > 0) {
1090
+ baseQuery = this.applyNestedJoins(baseQuery, include.nested);
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ // Apply where clause
1096
+ if (whereClause) {
1097
+ baseQuery = baseQuery.where(whereClause);
1098
+ }
1099
+ // Apply ordering
1100
+ if (orderByClause && orderByClause.length > 0) {
1101
+ baseQuery = baseQuery.orderBy(...orderByClause);
1102
+ }
1103
+ // Apply pagination
1104
+ if (limit !== undefined && limit !== -1) {
1105
+ baseQuery = baseQuery.limit(limit);
1106
+ }
1107
+ if (offset !== undefined) {
1108
+ baseQuery = baseQuery.offset(offset);
1109
+ }
1110
+ // Generate cache key and get involved tables
1111
+ const cacheKey = this.generateCacheKey(modifiedQuery, processedIncludes);
1112
+ const involvedTables = this.getInvolvedTables(processedIncludes);
1113
+ // Execute main query with cache
1114
+ const { records: finalRecords, totalCount } = await this.executeWithCache(cacheKey, involvedTables, async () => {
1115
+ // Execute main query
1116
+ let records;
1117
+ try {
1118
+ records = await baseQuery;
1119
+ }
1120
+ catch (queryError) {
1121
+ console.error(`[ItemsService.readByQuery] Query execution failed for ${this.collection}`);
1122
+ console.error(`[ItemsService.readByQuery] selectColumns:`, Object.keys(selectColumns));
1123
+ console.error(`[ItemsService.readByQuery] whereClause exists:`, !!whereClause);
1124
+ console.error(`[ItemsService.readByQuery] orderByClause exists:`, !!orderByClause);
1125
+ console.error(`[ItemsService.readByQuery] Query error:`, queryError.message);
1126
+ throw queryError;
1127
+ }
1128
+ // Note: Deduplication for filterJoins is now handled by readWithHasManyHandling
1129
+ // which uses a two-query approach (fetch IDs first, deduplicate, then fetch full records)
1130
+ // Load separate relations (HasMany, BelongsToMany) and nest joined relations
1131
+ let processedRecords = records;
1132
+ if (hasSeparateQueries(processedIncludes)) {
1133
+ processedRecords = await loadSeparateRelations(db, records, processedIncludes, this.collection);
1134
+ }
1135
+ else {
1136
+ // No separate queries, but we still need to nest joined BelongsTo/HasOne relations
1137
+ processedRecords = nestJoinedRelations(records, processedIncludes);
1138
+ }
1139
+ // Get total count
1140
+ const primaryKeyColumn = this.getPrimaryKeyColumn();
1141
+ if (!primaryKeyColumn) {
1142
+ console.error(`[ItemsService.readByQuery] Primary key column is undefined for ${this.collection}!`);
1143
+ console.error(`[ItemsService.readByQuery] Primary key name:`, this.primaryKey);
1144
+ console.error(`[ItemsService.readByQuery] Available columns:`, Object.keys(this.table).filter(k => !k.startsWith('_')));
1145
+ }
1146
+ let countQuery = db.select({ count: sql `COUNT(DISTINCT ${primaryKeyColumn})` }).from(this.table);
1147
+ // Apply same joins and where for count
1148
+ if (hasFilterJoins) {
1149
+ // Use filterJoins for count query as well
1150
+ for (const filterJoin of filterJoins) {
1151
+ const aliasedTable = alias(filterJoin.table, filterJoin.alias);
1152
+ countQuery = countQuery.leftJoin(aliasedTable, filterJoin.condition);
1153
+ }
1154
+ }
1155
+ else {
1156
+ for (const include of processedIncludes) {
1157
+ if (include.separate)
1158
+ continue;
1159
+ if (include.relationType === 'BelongsTo' || include.relationType === 'HasOne') {
1160
+ countQuery = countQuery.leftJoin(include.table, include.joinCondition);
1161
+ if (include.nested.length > 0) {
1162
+ countQuery = this.applyNestedJoins(countQuery, include.nested);
1163
+ }
1164
+ }
1165
+ }
1166
+ }
1167
+ if (whereClause) {
1168
+ countQuery = countQuery.where(whereClause);
1169
+ }
1170
+ let countResult, count;
1171
+ try {
1172
+ countResult = await countQuery;
1173
+ count = Number(countResult[0]?.count || 0);
1174
+ }
1175
+ catch (countError) {
1176
+ console.error(`[ItemsService.readByQuery] Count query execution failed for ${this.collection}`);
1177
+ console.error(`[ItemsService.readByQuery] Count query error:`, countError.message);
1178
+ console.error(`[ItemsService.readByQuery] HasFilterJoins:`, hasFilterJoins);
1179
+ console.error(`[ItemsService.readByQuery] ProcessedIncludes count:`, processedIncludes.length);
1180
+ throw countError;
1181
+ }
1182
+ // Return both records and count to be cached together
1183
+ return {
1184
+ records: processedRecords,
1185
+ totalCount: count
1186
+ };
1187
+ });
1188
+ // Strip hidden fields from records
1189
+ const strippedRecords = fieldUtils.stripHiddenFieldsFromRecords(this.collection, finalRecords);
1190
+ // Sanitize auto-added fields (remove fields user didn't request)
1191
+ const sanitizedRecords = this.sanitizeAutoAddedFields(strippedRecords, userRequestedFields, userRequestedIncludes);
1192
+ // Execute after-read hooks
1193
+ hookData = await hooksManager.executeHooks(this.collection, 'items.read.after', this.accountability, { query: modifiedQuery, result: { data: sanitizedRecords, totalCount } });
1194
+ return hookData.result;
1195
+ }
1196
+ catch (error) {
1197
+ console.error('Error in readByQuery:', error);
1198
+ throw error;
1199
+ }
1200
+ }
1201
+ /**
1202
+ * Execute aggregate query
1203
+ */
1204
+ async executeAggregateQuery(query, isAdmin, bypassPermissions) {
1205
+ const { aggregate, groupBy = [], filter = {}, sort } = query;
1206
+ if (!aggregate) {
1207
+ throw new APIError('Aggregate query requires aggregate parameter', 400);
1208
+ }
1209
+ // Extract all relation paths from the query
1210
+ const relationPaths = new Set();
1211
+ // Helper to check if a dotted path is a qualified main table reference vs a relation path
1212
+ // e.g., "baasix_User.createdAt" when querying baasix_User - check if "createdAt" is a column
1213
+ const isMainTableQualifiedField = (field) => {
1214
+ const parts = field.split('.');
1215
+ if (parts.length !== 2)
1216
+ return false;
1217
+ const [tablePart, columnPart] = parts;
1218
+ if (tablePart !== this.collection)
1219
+ return false;
1220
+ // Check if the column exists on the main table
1221
+ return columnPart in this.table;
1222
+ };
1223
+ // From groupBy fields
1224
+ for (const field of groupBy) {
1225
+ if (field.includes('.') && !field.startsWith('date:')) {
1226
+ // Skip if this is a qualified reference to a main table column (e.g., "baasix_User.createdAt")
1227
+ if (!isMainTableQualifiedField(field)) {
1228
+ relationPaths.add(field);
1229
+ }
1230
+ }
1231
+ }
1232
+ // From aggregate field specifications
1233
+ for (const [, aggregateInfo] of Object.entries(aggregate)) {
1234
+ const field = aggregateInfo.field;
1235
+ if (field && field.includes('.')) {
1236
+ // Skip if this is a qualified reference to a main table column
1237
+ if (!isMainTableQualifiedField(field)) {
1238
+ relationPaths.add(field);
1239
+ }
1240
+ }
1241
+ }
1242
+ // Apply filter with permissions and tenant context
1243
+ let combinedFilter = filter;
1244
+ if (!bypassPermissions && !isAdmin) {
1245
+ const roleId = this.getRoleId();
1246
+ // First, check if user has permission to read this collection
1247
+ const hasAccess = await permissionService.canAccess(roleId, this.collection, 'read');
1248
+ if (!hasAccess) {
1249
+ throw new APIError(`You don't have permission to read items in '${this.collection}'`, 403);
1250
+ }
1251
+ // Then apply permission filters
1252
+ const permissionFilter = await permissionService.getFilter(roleId, this.collection, 'read', this.accountability);
1253
+ if (permissionFilter.conditions) {
1254
+ combinedFilter = combineFilters(combinedFilter, permissionFilter.conditions);
1255
+ }
1256
+ }
1257
+ combinedFilter = await this.enforceTenantContextFilter(combinedFilter);
1258
+ combinedFilter = await resolveDynamicVariables(combinedFilter, this.accountability);
1259
+ // Build WHERE clause and accumulate filter joins
1260
+ const filterJoins = [];
1261
+ const whereClause = drizzleWhere(combinedFilter, {
1262
+ table: this.table,
1263
+ tableName: this.collection,
1264
+ schema: this.table,
1265
+ joins: filterJoins
1266
+ });
1267
+ // Deduplicate filter joins by alias
1268
+ const uniqueFilterJoins = [];
1269
+ const seenAliases = new Set();
1270
+ for (const join of filterJoins) {
1271
+ if (!seenAliases.has(join.alias)) {
1272
+ seenAliases.add(join.alias);
1273
+ uniqueFilterJoins.push(join);
1274
+ }
1275
+ }
1276
+ filterJoins.length = 0;
1277
+ filterJoins.push(...uniqueFilterJoins);
1278
+ // Resolve all relation paths to joins
1279
+ const allJoins = [...filterJoins];
1280
+ const pathToAliasMap = new Map(); // Maps relation path to final table alias
1281
+ // Add the main table to the alias map so fields like "baasix_User.createdAt" get resolved correctly
1282
+ pathToAliasMap.set(this.collection, this.collection);
1283
+ for (const relationPath of relationPaths) {
1284
+ try {
1285
+ const resolved = resolveRelationPath(relationPath, this.table, this.collection);
1286
+ // Store the mapping from path to final alias
1287
+ pathToAliasMap.set(relationPath, resolved.finalAlias);
1288
+ // Add any new joins (avoid duplicates)
1289
+ for (const join of resolved.joins) {
1290
+ const exists = allJoins.some(j => j.alias === join.alias);
1291
+ if (!exists) {
1292
+ allJoins.push(join);
1293
+ }
1294
+ }
1295
+ }
1296
+ catch (error) {
1297
+ console.warn(`[ItemsService] Could not resolve relation path ${relationPath}:`, error.message);
1298
+ }
1299
+ }
1300
+ // Build aggregate attributes with alias mapping for relations
1301
+ const ctx = { tableName: this.collection, pathToAliasMap };
1302
+ const attributes = buildAggregateAttributes(aggregate, groupBy, ctx);
1303
+ // Build select object
1304
+ const selectObj = {};
1305
+ // Add group by fields with alias mapping for relations
1306
+ for (const groupField of groupBy) {
1307
+ const groupExpr = buildGroupByExpressions([groupField], undefined, pathToAliasMap)[0];
1308
+ selectObj[groupField] = groupExpr;
1309
+ }
1310
+ // Add aggregate functions
1311
+ for (const [expr, alias] of attributes) {
1312
+ selectObj[alias] = expr;
1313
+ }
1314
+ // Execute aggregate query using Drizzle query builder
1315
+ let results;
1316
+ if (allJoins.length > 0) {
1317
+ // Build aggregate query using Drizzle query builder
1318
+ let aggregateQuery = db.select(selectObj).from(this.table).$dynamic();
1319
+ // Apply joins (same as filterJoins)
1320
+ for (const join of allJoins) {
1321
+ // Create aliased table with the exact alias
1322
+ const aliasedTable = alias(join.table, join.alias);
1323
+ // Apply join using Drizzle's leftJoin (or other join type)
1324
+ const joinMethod = join.type === 'inner' ? 'innerJoin' :
1325
+ join.type === 'right' ? 'rightJoin' : 'leftJoin';
1326
+ aggregateQuery = aggregateQuery[joinMethod](aliasedTable, join.condition);
1327
+ }
1328
+ // Apply WHERE clause
1329
+ if (whereClause) {
1330
+ aggregateQuery = aggregateQuery.where(whereClause);
1331
+ }
1332
+ // Apply GROUP BY with alias mapping for relations
1333
+ if (groupBy.length > 0) {
1334
+ const groupByExprs = buildGroupByExpressions(groupBy, undefined, pathToAliasMap);
1335
+ aggregateQuery = aggregateQuery.groupBy(...groupByExprs);
1336
+ }
1337
+ // Apply ORDER BY - for aggregate queries, check if sorting by aggregate alias
1338
+ if (sort) {
1339
+ const sortObj = typeof sort === 'string' ? JSON.parse(sort) : sort;
1340
+ const orderByClause = [];
1341
+ for (const [field, direction] of Object.entries(sortObj)) {
1342
+ // Check if this field is an aggregate alias in selectObj
1343
+ if (selectObj[field]) {
1344
+ // Use the aggregate expression from selectObj directly
1345
+ const normalizedDirection = direction.toUpperCase();
1346
+ const expr = selectObj[field];
1347
+ orderByClause.push(normalizedDirection === 'ASC' ? asc(expr) : desc(expr));
1348
+ }
1349
+ else {
1350
+ // Use drizzleOrder for non-aggregate fields
1351
+ const clause = drizzleOrder({ [field]: direction }, {
1352
+ table: this.table,
1353
+ tableName: this.collection
1354
+ });
1355
+ orderByClause.push(...clause);
1356
+ }
1357
+ }
1358
+ if (orderByClause.length > 0) {
1359
+ aggregateQuery = aggregateQuery.orderBy(...orderByClause);
1360
+ }
1361
+ }
1362
+ // Execute query
1363
+ results = await aggregateQuery;
1364
+ }
1365
+ else {
1366
+ // No relation joins - use standard Drizzle API
1367
+ let aggregateQuery = db.select(selectObj).from(this.table).$dynamic();
1368
+ if (whereClause) {
1369
+ aggregateQuery = aggregateQuery.where(whereClause);
1370
+ }
1371
+ if (groupBy.length > 0) {
1372
+ const groupByExprs = buildGroupByExpressions(groupBy, undefined, pathToAliasMap);
1373
+ aggregateQuery = aggregateQuery.groupBy(...groupByExprs);
1374
+ }
1375
+ // Apply sorting if provided - for aggregate queries, check if sorting by aggregate alias
1376
+ if (sort) {
1377
+ const sortObj = typeof sort === 'string' ? JSON.parse(sort) : sort;
1378
+ const orderByClause = [];
1379
+ for (const [field, direction] of Object.entries(sortObj)) {
1380
+ // Check if this field is an aggregate alias in selectObj
1381
+ if (selectObj[field]) {
1382
+ // Use the aggregate expression from selectObj directly
1383
+ const normalizedDirection = direction.toUpperCase();
1384
+ const expr = selectObj[field];
1385
+ orderByClause.push(normalizedDirection === 'ASC' ? asc(expr) : desc(expr));
1386
+ }
1387
+ else {
1388
+ // Use drizzleOrder for non-aggregate fields
1389
+ const clause = drizzleOrder({ [field]: direction }, {
1390
+ table: this.table,
1391
+ tableName: this.collection
1392
+ });
1393
+ orderByClause.push(...clause);
1394
+ }
1395
+ }
1396
+ if (orderByClause.length > 0) {
1397
+ aggregateQuery = aggregateQuery.orderBy(...orderByClause);
1398
+ }
1399
+ }
1400
+ // Execute query
1401
+ results = await aggregateQuery;
1402
+ }
1403
+ // For grouped results, count is the number of groups
1404
+ const totalCount = results.length;
1405
+ return {
1406
+ data: results,
1407
+ totalCount
1408
+ };
1409
+ }
1410
+ /**
1411
+ * Read a single record by ID
1412
+ */
1413
+ async readOne(id, query = {}, bypassPermissions = false) {
1414
+ const parsedId = this.parseId(id);
1415
+ if (!parsedId) {
1416
+ throw new APIError('Invalid ID', 400);
1417
+ }
1418
+ // Execute before-read-one hooks
1419
+ let hookData = await hooksManager.executeHooks(this.collection, 'items.read.one', this.accountability, { id: parsedId, query });
1420
+ try {
1421
+ const isAdmin = await this.isAdministrator();
1422
+ // Build query with ID filter
1423
+ const { whereClause, selectColumns, joins, processedIncludes, filterJoins } = await this.buildQuery(query, {
1424
+ isAdmin,
1425
+ action: 'read',
1426
+ bypassPermissions,
1427
+ idFilter: parsedId
1428
+ });
1429
+ // Build base query
1430
+ let baseQuery = db.select(selectColumns).from(this.table);
1431
+ // Apply filterJoins first (these come from relation path filters in WHERE clause)
1432
+ // use Drizzle query builder methods instead of raw SQL
1433
+ if (filterJoins && filterJoins.length > 0) {
1434
+ filterJoins.forEach((join) => {
1435
+ const { table: joinTable, condition, type = 'left' } = join;
1436
+ const joinMethod = type === 'inner' ? 'innerJoin' : 'leftJoin';
1437
+ baseQuery = baseQuery[joinMethod](joinTable, condition);
1438
+ });
1439
+ }
1440
+ // Apply joins for includes (these are for loading related data)
1441
+ for (const include of processedIncludes) {
1442
+ if (include.separate)
1443
+ continue;
1444
+ if (include.relationType === 'BelongsTo' || include.relationType === 'HasOne') {
1445
+ baseQuery = baseQuery.leftJoin(include.table, include.joinCondition);
1446
+ if (include.nested.length > 0) {
1447
+ baseQuery = this.applyNestedJoins(baseQuery, include.nested);
1448
+ }
1449
+ }
1450
+ }
1451
+ // Apply where clause
1452
+ if (whereClause) {
1453
+ baseQuery = baseQuery.where(whereClause);
1454
+ }
1455
+ // Execute query
1456
+ const records = await baseQuery.limit(1);
1457
+ if (!records || records.length === 0) {
1458
+ throw new APIError("Item not found or you don't have permission to read it", 403);
1459
+ }
1460
+ // Load separate relations and nest joined relations
1461
+ let finalRecords = records;
1462
+ if (hasSeparateQueries(processedIncludes)) {
1463
+ finalRecords = await loadSeparateRelations(db, records, processedIncludes, this.collection);
1464
+ }
1465
+ else {
1466
+ // No separate queries, but we still need to nest joined BelongsTo/HasOne relations
1467
+ finalRecords = nestJoinedRelations(records, processedIncludes);
1468
+ }
1469
+ const document = finalRecords[0];
1470
+ // Strip hidden fields from the document
1471
+ const strippedDocument = fieldUtils.stripHiddenFields(this.collection, document);
1472
+ // Execute after-read-one hooks
1473
+ hookData = await hooksManager.executeHooks(this.collection, 'items.read.one.after', this.accountability, { id: parsedId, query, document: strippedDocument });
1474
+ return hookData.document;
1475
+ }
1476
+ catch (error) {
1477
+ console.error('Error in readOne:', error);
1478
+ throw error;
1479
+ }
1480
+ }
1481
+ /**
1482
+ * Create a new record
1483
+ * Uses createOneCore for the transactional logic and executes after hooks after commit
1484
+ */
1485
+ async createOne(data, options = {}) {
1486
+ console.log(`[ItemsService.createOne] START - Collection: ${this.collection}`);
1487
+ console.log('[ItemsService.createOne] Input data:', JSON.stringify(data));
1488
+ // Create transaction if not provided (matches Sequelize pattern)
1489
+ const transaction = options.transaction || (await createTransaction());
1490
+ const shouldCommit = !options.transaction; // Only commit if we created the transaction
1491
+ try {
1492
+ // Execute core create logic within transaction
1493
+ const result = await this.createOneCore(data, transaction, options);
1494
+ // Commit transaction if we created it
1495
+ if (shouldCommit) {
1496
+ await transaction.commit();
1497
+ }
1498
+ // Create audit log for create action (after commit for single operations)
1499
+ await this.createAuditLog('create', result.itemId, {
1500
+ before: null,
1501
+ after: result.document
1502
+ }, options.transaction);
1503
+ // Execute after-create hooks (after commit to prevent side effects on rollback)
1504
+ await hooksManager.executeHooks(this.collection, 'items.create.after', this.accountability, { data: result.modifiedData, document: result.document, transaction: options.transaction });
1505
+ // Invalidate cache for this collection and all related tables
1506
+ await this.invalidateCache(result.relatedTables);
1507
+ return result.itemId;
1508
+ }
1509
+ catch (error) {
1510
+ console.error('Error in createOne:', error);
1511
+ // Rollback transaction if we created it
1512
+ if (shouldCommit) {
1513
+ try {
1514
+ await transaction.rollback();
1515
+ }
1516
+ catch (rollbackError) {
1517
+ // Ignore __ROLLBACK__ errors as they're intentional
1518
+ if (rollbackError.message !== '__ROLLBACK__') {
1519
+ console.error('Error during rollback:', rollbackError);
1520
+ }
1521
+ }
1522
+ }
1523
+ throw error;
1524
+ }
1525
+ }
1526
+ /**
1527
+ * Internal method to perform core create logic without after hooks
1528
+ * Used by both createOne and createMany to separate transactional data operations
1529
+ * from after hooks that may have side effects (emails, third-party calls)
1530
+ *
1531
+ * @param data - Item data to create
1532
+ * @param transaction - Transaction to use
1533
+ * @param options - Operation options
1534
+ * @returns Object containing item ID, document, modified data, and related tables for cache invalidation
1535
+ */
1536
+ async createOneCore(data, transaction, options = {}) {
1537
+ console.log(`[ItemsService.createOneCore] START - Collection: ${this.collection}`);
1538
+ console.log('[ItemsService.createOneCore] Input data:', JSON.stringify(data));
1539
+ // Execute before-create hooks with transaction
1540
+ let hookData = await hooksManager.executeHooks(this.collection, 'items.create', this.accountability, { data, transaction: options.transaction });
1541
+ let modifiedData = hookData.data;
1542
+ console.log('[ItemsService.createOneCore] After before-hooks, modifiedData:', JSON.stringify(modifiedData));
1543
+ // Hash password for baasix_User
1544
+ if (this.collection === 'baasix_User' && modifiedData.password) {
1545
+ console.log('[ItemsService.createOneCore] Hashing password for baasix_User');
1546
+ modifiedData.password = await argon2.hash(modifiedData.password);
1547
+ }
1548
+ const isAdmin = await this.isAdministrator();
1549
+ // Apply field permissions
1550
+ if (!options.bypassPermissions) {
1551
+ await this.applyFieldPermissions(modifiedData, 'create', isAdmin);
1552
+ const defaultValues = await this.getDefaultValues('create');
1553
+ modifiedData = { ...defaultValues, ...modifiedData };
1554
+ }
1555
+ // Validate and enforce tenant context
1556
+ modifiedData = await this.validateAndEnforceTenantContext(modifiedData);
1557
+ // Validate relational data
1558
+ await validateRelationalData(modifiedData, this.collection, this);
1559
+ // Validate field values against schema validation rules (min, max, etc.)
1560
+ await valueValidator.validateOrThrow(this.collection, modifiedData, false);
1561
+ // Handle circular dependencies
1562
+ const { resolvedData, deferredFields } = await resolveCircularDependencies(modifiedData, this.collection, this);
1563
+ // Process relational data (extract nested objects/arrays)
1564
+ const relationalResult = await processRelationalData(this.collection, resolvedData, this, ItemsService);
1565
+ const { result: mainData, deferredM2M, deferredM2A, deferredHasMany } = relationalResult;
1566
+ // Handle usertrack: set userCreated_Id if enabled
1567
+ const schemaDefinition = await schemaManager.getSchemaDefinition(this.collection);
1568
+ if (schemaDefinition?.usertrack && this.accountability?.user?.id) {
1569
+ mainData.userCreated_Id = this.accountability.user.id;
1570
+ }
1571
+ // Handle sortEnabled: auto-assign sequential sort values
1572
+ if (schemaDefinition?.sortEnabled && !mainData.sort) {
1573
+ try {
1574
+ const maxSortResult = await transaction.execute(sql `SELECT COALESCE(MAX("sort"), 0) as "maxSort" FROM ${sql.raw(`"${this.collection}"`)}`);
1575
+ let maxSort = 0;
1576
+ if (maxSortResult && Array.isArray(maxSortResult)) {
1577
+ maxSort = maxSortResult[0]?.maxSort ?? 0;
1578
+ }
1579
+ else if (maxSortResult?.rows && Array.isArray(maxSortResult.rows)) {
1580
+ maxSort = maxSortResult.rows[0]?.maxSort ?? 0;
1581
+ }
1582
+ mainData.sort = Number(maxSort) + 1;
1583
+ }
1584
+ catch (sortError) {
1585
+ console.error('[ItemsService.createOneCore] Error assigning sort value:', sortError);
1586
+ }
1587
+ }
1588
+ // Filter out VIRTUAL (generated) fields
1589
+ if (schemaDefinition?.fields) {
1590
+ for (const [fieldName, fieldSchema] of Object.entries(schemaDefinition.fields)) {
1591
+ if (fieldSchema.type === 'VIRTUAL' && fieldName in mainData) {
1592
+ delete mainData[fieldName];
1593
+ }
1594
+ }
1595
+ }
1596
+ // Remove undefined values before insert
1597
+ const cleanedData = {};
1598
+ for (const [key, value] of Object.entries(mainData)) {
1599
+ if (value !== undefined) {
1600
+ cleanedData[key] = value;
1601
+ }
1602
+ }
1603
+ // Convert date strings to Date objects for DateTime/Timestamp fields
1604
+ await this.convertDateFields(cleanedData, schemaDefinition);
1605
+ // Insert main record
1606
+ let item;
1607
+ try {
1608
+ const insertResult = await transaction
1609
+ .insert(this.table)
1610
+ .values(cleanedData)
1611
+ .returning();
1612
+ if (!insertResult || insertResult.length === 0) {
1613
+ throw new APIError('Failed to create item', 500);
1614
+ }
1615
+ item = insertResult[0];
1616
+ }
1617
+ catch (insertError) {
1618
+ console.error('Insert error details:', {
1619
+ collection: this.collection,
1620
+ error: insertError.message,
1621
+ cleanedDataKeys: Object.keys(cleanedData),
1622
+ cleanedData: cleanedData
1623
+ });
1624
+ throw insertError;
1625
+ }
1626
+ const itemId = item[this.primaryKey];
1627
+ // Process deferred fields (circular dependencies)
1628
+ await processDeferredFields(item, deferredFields, this, transaction);
1629
+ // Handle deferred HasMany relations
1630
+ for (const { association, associationInfo, value } of deferredHasMany) {
1631
+ await handleHasManyRelationship(item, association, associationInfo, value, this, ItemsService, transaction);
1632
+ }
1633
+ // Handle M2M relationships
1634
+ for (const { association, associationInfo, value } of deferredM2M) {
1635
+ await handleM2MRelationship(item, association, associationInfo, value, this, ItemsService, transaction);
1636
+ }
1637
+ // Handle M2A relationships
1638
+ for (const { association, associationInfo, value } of deferredM2A) {
1639
+ await handleM2ARelationship(item, association, associationInfo, value, this, ItemsService, transaction);
1640
+ }
1641
+ // Collect related tables for cache invalidation
1642
+ const relatedTables = [];
1643
+ if (deferredM2M.length > 0) {
1644
+ for (const { associationInfo } of deferredM2M) {
1645
+ if (associationInfo.junctionTable) {
1646
+ relatedTables.push(associationInfo.junctionTable);
1647
+ }
1648
+ }
1649
+ }
1650
+ if (deferredHasMany.length > 0) {
1651
+ for (const { associationInfo } of deferredHasMany) {
1652
+ if (associationInfo.relatedCollection) {
1653
+ relatedTables.push(associationInfo.relatedCollection);
1654
+ }
1655
+ }
1656
+ }
1657
+ if (deferredM2A.length > 0) {
1658
+ for (const { associationInfo } of deferredM2A) {
1659
+ if (associationInfo.relatedCollections) {
1660
+ relatedTables.push(...associationInfo.relatedCollections);
1661
+ }
1662
+ }
1663
+ }
1664
+ return {
1665
+ itemId,
1666
+ document: item,
1667
+ modifiedData,
1668
+ relatedTables
1669
+ };
1670
+ }
1671
+ /**
1672
+ * Create multiple records with transactional safety
1673
+ *
1674
+ * This method ensures that:
1675
+ * 1. All items are created within a single transaction
1676
+ * 2. If any creation fails, all previous creations are rolled back
1677
+ * 3. After hooks (which may have external side effects like emails, API calls)
1678
+ * are only executed AFTER the transaction is successfully committed
1679
+ *
1680
+ * This prevents situations where:
1681
+ * - Emails are sent for items that were later rolled back
1682
+ * - Third-party systems are notified about changes that didn't persist
1683
+ *
1684
+ * @param items - Array of items to create
1685
+ * @param options - Operation options
1686
+ * @returns Array of created item IDs
1687
+ */
1688
+ async createMany(items, options = {}) {
1689
+ if (items.length === 0) {
1690
+ return [];
1691
+ }
1692
+ // Create transaction if not provided
1693
+ const transaction = options.transaction || (await createTransaction());
1694
+ const shouldCommit = !options.transaction; // Only commit if we created the transaction
1695
+ // Store results for after hooks
1696
+ const results = [];
1697
+ try {
1698
+ // Phase 1: Execute all core create operations within transaction
1699
+ for (const item of items) {
1700
+ const result = await this.createOneCore(item, transaction, options);
1701
+ results.push(result);
1702
+ }
1703
+ // Phase 2: Commit transaction (if we created it)
1704
+ if (shouldCommit) {
1705
+ await transaction.commit();
1706
+ }
1707
+ // Phase 3: Execute after hooks AFTER successful commit
1708
+ // These may have external side effects (emails, third-party calls)
1709
+ // that cannot be rolled back, so we only execute them after commit
1710
+ for (const result of results) {
1711
+ // Create audit log
1712
+ await this.createAuditLog('create', result.itemId, {
1713
+ before: null,
1714
+ after: result.document
1715
+ }, options.transaction);
1716
+ // Execute after-create hooks
1717
+ await hooksManager.executeHooks(this.collection, 'items.create.after', this.accountability, { data: result.modifiedData, document: result.document, transaction: options.transaction });
1718
+ }
1719
+ // Invalidate cache for all affected tables
1720
+ const allRelatedTables = [...new Set(results.flatMap(r => r.relatedTables))];
1721
+ await this.invalidateCache(allRelatedTables);
1722
+ return results.map(r => r.itemId);
1723
+ }
1724
+ catch (error) {
1725
+ console.error('Error in createMany:', error);
1726
+ // Rollback transaction if we created it
1727
+ if (shouldCommit) {
1728
+ try {
1729
+ await transaction.rollback();
1730
+ }
1731
+ catch (rollbackError) {
1732
+ if (rollbackError.message !== '__ROLLBACK__') {
1733
+ console.error('Error during rollback:', rollbackError);
1734
+ }
1735
+ }
1736
+ }
1737
+ throw error;
1738
+ }
1739
+ }
1740
+ /**
1741
+ * Internal method to perform core update logic without after hooks
1742
+ * Used by both updateOne and updateMany to separate transactional data operations
1743
+ * from after hooks that may have side effects (emails, third-party calls)
1744
+ *
1745
+ * @param id - Item ID to update
1746
+ * @param data - Item data to update
1747
+ * @param transaction - Transaction to use
1748
+ * @param options - Operation options
1749
+ * @returns Object containing update info for after hooks execution
1750
+ */
1751
+ async updateOneCore(id, data, transaction, options = {}) {
1752
+ const parsedId = this.parseId(id);
1753
+ // Execute before-update hooks with transaction
1754
+ let hookData = await hooksManager.executeHooks(this.collection, 'items.update', this.accountability, { id: parsedId, data, transaction: options.transaction });
1755
+ let modifiedData = hookData.data;
1756
+ // Hash password for baasix_User
1757
+ if (this.collection === 'baasix_User' && modifiedData.password) {
1758
+ modifiedData.password = await argon2.hash(modifiedData.password);
1759
+ }
1760
+ const isAdmin = await this.isAdministrator();
1761
+ // Apply field permissions
1762
+ if (!options.bypassPermissions) {
1763
+ await this.applyFieldPermissions(modifiedData, 'update', isAdmin);
1764
+ const defaultValues = await this.getDefaultValues('update');
1765
+ modifiedData = { ...defaultValues, ...modifiedData };
1766
+ }
1767
+ // Validate and enforce tenant context
1768
+ modifiedData = await this.validateAndEnforceTenantContext(modifiedData);
1769
+ // Validate field values against schema validation rules (min, max, etc.)
1770
+ await valueValidator.validateOrThrow(this.collection, modifiedData, true);
1771
+ // Filter out VIRTUAL (generated) fields
1772
+ let schemaDefinition = await schemaManager.getSchemaDefinition(this.collection);
1773
+ if (schemaDefinition?.fields) {
1774
+ for (const [fieldName, fieldSchema] of Object.entries(schemaDefinition.fields)) {
1775
+ if (fieldSchema.type === 'VIRTUAL' && fieldName in modifiedData) {
1776
+ delete modifiedData[fieldName];
1777
+ }
1778
+ }
1779
+ }
1780
+ // Build filter for existing record check
1781
+ let filter = {
1782
+ [this.primaryKey]: parsedId
1783
+ };
1784
+ if (!options.bypassPermissions && !isAdmin) {
1785
+ const roleId = this.getRoleId();
1786
+ const permissionFilter = await permissionService.getFilter(roleId, this.collection, 'update', this.accountability);
1787
+ if (permissionFilter.conditions) {
1788
+ filter = combineFilters(filter, permissionFilter.conditions);
1789
+ }
1790
+ }
1791
+ filter = await this.enforceTenantContextFilter(filter);
1792
+ filter = await resolveDynamicVariables(filter, this.accountability);
1793
+ // Check if record exists and user has permission
1794
+ const filterJoins = [];
1795
+ const whereClause = drizzleWhere(filter, {
1796
+ table: this.table,
1797
+ tableName: this.collection,
1798
+ schema: this.table,
1799
+ joins: filterJoins,
1800
+ forPermissionCheck: true
1801
+ });
1802
+ // Deduplicate filter joins by alias
1803
+ const uniqueJoins = [];
1804
+ const seenAliases = new Set();
1805
+ for (const join of filterJoins) {
1806
+ if (!seenAliases.has(join.alias)) {
1807
+ seenAliases.add(join.alias);
1808
+ uniqueJoins.push(join);
1809
+ }
1810
+ }
1811
+ filterJoins.length = 0;
1812
+ filterJoins.push(...uniqueJoins);
1813
+ let existingItems;
1814
+ if (filterJoins.length > 0) {
1815
+ // Use transaction for reads to prevent deadlocks and ensure consistency
1816
+ let query = transaction.select().from(this.table);
1817
+ filterJoins.forEach((join) => {
1818
+ const { table: joinTable, condition, type = 'left' } = join;
1819
+ const joinMethod = type === 'inner' ? 'innerJoin' : 'leftJoin';
1820
+ query = query[joinMethod](joinTable, condition);
1821
+ });
1822
+ existingItems = await query.where(whereClause).limit(1);
1823
+ }
1824
+ else {
1825
+ // Use transaction for reads to prevent deadlocks and ensure consistency
1826
+ existingItems = await transaction
1827
+ .select()
1828
+ .from(this.table)
1829
+ .where(whereClause)
1830
+ .limit(1);
1831
+ }
1832
+ if (!existingItems || existingItems.length === 0) {
1833
+ throw new APIError("Item not found or you don't have permission to update it", 403);
1834
+ }
1835
+ // When there are joins, Drizzle returns results in nested format: { tableName: { ...data }, joinedTable: { ...data } }
1836
+ // Extract the main table's data when joins are present
1837
+ let existingItem = existingItems[0];
1838
+ if (filterJoins.length > 0 && existingItem[this.collection]) {
1839
+ existingItem = existingItem[this.collection];
1840
+ }
1841
+ // Ensure tenant_Id cannot be changed (except by admin)
1842
+ if (this.isMultiTenant &&
1843
+ modifiedData.tenant_Id &&
1844
+ modifiedData.tenant_Id !== existingItem.tenant_Id &&
1845
+ !isAdmin) {
1846
+ throw new APIError("Cannot change item's tenant", 403);
1847
+ }
1848
+ // Process relational data
1849
+ const relationalResult = await processRelationalData(this.collection, modifiedData, this, ItemsService);
1850
+ const { result: mainData, deferredM2M, deferredM2A, deferredHasMany } = relationalResult;
1851
+ // Handle usertrack: set userUpdated_Id if enabled
1852
+ if (!schemaDefinition) {
1853
+ schemaDefinition = await schemaManager.getSchemaDefinition(this.collection);
1854
+ }
1855
+ if (schemaDefinition?.usertrack && this.accountability?.user?.id) {
1856
+ mainData.userUpdated_Id = this.accountability.user.id;
1857
+ }
1858
+ // Auto-update updatedAt timestamp if timestamps enabled (default: true)
1859
+ if (schemaDefinition?.timestamps !== false && 'updatedAt' in this.table) {
1860
+ mainData.updatedAt = new Date();
1861
+ }
1862
+ // Convert date strings to Date objects for DateTime/Timestamp fields
1863
+ await this.convertDateFields(mainData, schemaDefinition);
1864
+ // Update main record only if there are fields to update
1865
+ let updatedItem;
1866
+ if (Object.keys(mainData).length > 0) {
1867
+ const updateResult = await transaction
1868
+ .update(this.table)
1869
+ .set(mainData)
1870
+ .where(eq(this.getPrimaryKeyColumn(), parsedId))
1871
+ .returning();
1872
+ if (!updateResult || updateResult.length === 0) {
1873
+ if (deferredM2M.length === 0 && deferredM2A.length === 0 && deferredHasMany.length === 0) {
1874
+ // No update needed, return existing item info
1875
+ return {
1876
+ parsedId,
1877
+ modifiedData,
1878
+ finalDocument: existingItem,
1879
+ previousDocument: existingItem,
1880
+ relatedTables: []
1881
+ };
1882
+ }
1883
+ }
1884
+ updatedItem = updateResult[0];
1885
+ }
1886
+ else {
1887
+ // Use transaction for reads to prevent deadlocks and ensure consistency
1888
+ const updatedItems = await transaction
1889
+ .select()
1890
+ .from(this.table)
1891
+ .where(eq(this.getPrimaryKeyColumn(), parsedId))
1892
+ .limit(1);
1893
+ updatedItem = updatedItems[0];
1894
+ }
1895
+ // Process deferred HasMany relations
1896
+ for (const { association, associationInfo, value } of deferredHasMany) {
1897
+ await handleHasManyRelationship(updatedItem, association, associationInfo, value, this, ItemsService, transaction);
1898
+ }
1899
+ // Handle M2M relationships
1900
+ for (const { association, associationInfo, value } of deferredM2M) {
1901
+ await handleM2MRelationship(updatedItem, association, associationInfo, value, this, ItemsService, transaction);
1902
+ }
1903
+ // Handle M2A relationships
1904
+ for (const { association, associationInfo, value } of deferredM2A) {
1905
+ await handleM2ARelationship(updatedItem, association, associationInfo, value, this, ItemsService, transaction);
1906
+ }
1907
+ // Get final item for hooks - use transaction for consistency
1908
+ const finalItems = await transaction
1909
+ .select()
1910
+ .from(this.table)
1911
+ .where(eq(this.getPrimaryKeyColumn(), parsedId))
1912
+ .limit(1);
1913
+ const finalItem = finalItems[0];
1914
+ // Collect related tables for cache invalidation
1915
+ const relatedTables = [];
1916
+ if (deferredM2M.length > 0) {
1917
+ for (const { associationInfo } of deferredM2M) {
1918
+ if (associationInfo.junctionTable) {
1919
+ relatedTables.push(associationInfo.junctionTable);
1920
+ }
1921
+ }
1922
+ }
1923
+ if (deferredHasMany.length > 0) {
1924
+ for (const { associationInfo } of deferredHasMany) {
1925
+ if (associationInfo.relatedCollection) {
1926
+ relatedTables.push(associationInfo.relatedCollection);
1927
+ }
1928
+ }
1929
+ }
1930
+ if (deferredM2A.length > 0) {
1931
+ for (const { associationInfo } of deferredM2A) {
1932
+ if (associationInfo.relatedCollections) {
1933
+ relatedTables.push(...associationInfo.relatedCollections);
1934
+ }
1935
+ }
1936
+ }
1937
+ return {
1938
+ parsedId,
1939
+ modifiedData,
1940
+ finalDocument: finalItem,
1941
+ previousDocument: existingItem,
1942
+ relatedTables
1943
+ };
1944
+ }
1945
+ /**
1946
+ * Update multiple records with transactional safety
1947
+ *
1948
+ * This method ensures that:
1949
+ * 1. All items are updated within a single transaction
1950
+ * 2. If any update fails, all previous updates are rolled back
1951
+ * 3. After hooks (which may have external side effects like emails, API calls)
1952
+ * are only executed AFTER the transaction is successfully committed
1953
+ *
1954
+ * @param updates - Array of objects with id and data to update
1955
+ * @param options - Operation options
1956
+ * @returns Array of updated item IDs
1957
+ */
1958
+ async updateMany(updates, options = {}) {
1959
+ if (updates.length === 0) {
1960
+ return [];
1961
+ }
1962
+ // Create transaction if not provided
1963
+ const transaction = options.transaction || (await createTransaction());
1964
+ const shouldCommit = !options.transaction;
1965
+ // Store results for after hooks
1966
+ const results = [];
1967
+ try {
1968
+ // Phase 1: Execute all core update operations within transaction
1969
+ for (const update of updates) {
1970
+ const { id, data, ...rest } = update;
1971
+ if (!id)
1972
+ continue;
1973
+ // Support both {id, data: {...}} and {id, field1, field2, ...} formats
1974
+ const updateData = data || rest;
1975
+ const result = await this.updateOneCore(id, updateData, transaction, options);
1976
+ results.push(result);
1977
+ }
1978
+ // Phase 2: Commit transaction (if we created it)
1979
+ if (shouldCommit) {
1980
+ await transaction.commit();
1981
+ }
1982
+ // Phase 3: Execute after hooks AFTER successful commit
1983
+ for (const result of results) {
1984
+ // Create audit log
1985
+ await this.createAuditLog('update', result.parsedId, {
1986
+ before: result.previousDocument,
1987
+ after: result.finalDocument
1988
+ }, options.transaction);
1989
+ // Execute after-update hooks
1990
+ await hooksManager.executeHooks(this.collection, 'items.update.after', this.accountability, {
1991
+ id: result.parsedId,
1992
+ data: result.modifiedData,
1993
+ document: result.finalDocument,
1994
+ previousDocument: result.previousDocument,
1995
+ transaction: options.transaction
1996
+ });
1997
+ }
1998
+ // Invalidate cache for all affected tables
1999
+ const allRelatedTables = [...new Set(results.flatMap(r => r.relatedTables))];
2000
+ await this.invalidateCache(allRelatedTables);
2001
+ return results.map(r => r.parsedId);
2002
+ }
2003
+ catch (error) {
2004
+ console.error('Error in updateMany:', error);
2005
+ if (shouldCommit) {
2006
+ try {
2007
+ await transaction.rollback();
2008
+ }
2009
+ catch (rollbackError) {
2010
+ if (rollbackError.message !== '__ROLLBACK__') {
2011
+ console.error('Error during rollback:', rollbackError);
2012
+ }
2013
+ }
2014
+ }
2015
+ throw error;
2016
+ }
2017
+ }
2018
+ /**
2019
+ * Update a record by ID
2020
+ * Uses updateOneCore for the transactional logic and executes after hooks after commit
2021
+ */
2022
+ async updateOne(id, data, options = {}) {
2023
+ // Create transaction if not provided (matches Sequelize pattern)
2024
+ const transaction = options.transaction || (await createTransaction());
2025
+ const shouldCommit = !options.transaction;
2026
+ try {
2027
+ // Execute core update logic within transaction
2028
+ const result = await this.updateOneCore(id, data, transaction, options);
2029
+ // Commit transaction if we created it
2030
+ if (shouldCommit) {
2031
+ await transaction.commit();
2032
+ }
2033
+ // Create audit log for update action (after commit)
2034
+ await this.createAuditLog('update', result.parsedId, {
2035
+ before: result.previousDocument,
2036
+ after: result.finalDocument
2037
+ }, options.transaction);
2038
+ // Execute after-update hooks (after commit to prevent side effects on rollback)
2039
+ await hooksManager.executeHooks(this.collection, 'items.update.after', this.accountability, {
2040
+ id: result.parsedId,
2041
+ data: result.modifiedData,
2042
+ document: result.finalDocument,
2043
+ previousDocument: result.previousDocument,
2044
+ transaction: options.transaction
2045
+ });
2046
+ // Invalidate cache for this collection and all related tables
2047
+ await this.invalidateCache(result.relatedTables);
2048
+ return result.parsedId;
2049
+ }
2050
+ catch (error) {
2051
+ console.error('Error in updateOne:', error);
2052
+ // Rollback transaction if we created it
2053
+ if (shouldCommit) {
2054
+ try {
2055
+ await transaction.rollback();
2056
+ }
2057
+ catch (rollbackError) {
2058
+ if (rollbackError.message !== '__ROLLBACK__') {
2059
+ console.error('Error during rollback:', rollbackError);
2060
+ }
2061
+ }
2062
+ }
2063
+ throw error;
2064
+ }
2065
+ }
2066
+ /**
2067
+ * Internal method to perform core delete logic without after hooks
2068
+ * Used by both deleteOne and deleteMany to separate transactional data operations
2069
+ * from after hooks that may have side effects (emails, third-party calls)
2070
+ *
2071
+ * @param id - Item ID to delete
2072
+ * @param transaction - Transaction to use
2073
+ * @param options - Operation options
2074
+ * @returns Object containing delete info for after hooks execution
2075
+ */
2076
+ async deleteOneCore(id, transaction, options = {}) {
2077
+ const parsedId = this.parseId(id);
2078
+ // Execute before-delete hooks with transaction
2079
+ await hooksManager.executeHooks(this.collection, 'items.delete', this.accountability, { id: parsedId, transaction: options.transaction });
2080
+ const isAdmin = await this.isAdministrator();
2081
+ // Check permission
2082
+ if (!options.bypassPermissions && !isAdmin) {
2083
+ const roleId = this.getRoleId();
2084
+ const hasPermission = await permissionService.canAccess(roleId, this.collection, 'delete');
2085
+ if (!hasPermission) {
2086
+ throw new APIError("You don't have permission to delete this item", 403);
2087
+ }
2088
+ }
2089
+ // Build filter for existing record check
2090
+ let filter = {
2091
+ [this.primaryKey]: parsedId
2092
+ };
2093
+ if (!options.bypassPermissions && !isAdmin) {
2094
+ const roleId = this.getRoleId();
2095
+ const permissionFilter = await permissionService.getFilter(roleId, this.collection, 'delete', this.accountability);
2096
+ if (permissionFilter.conditions) {
2097
+ filter = combineFilters(filter, permissionFilter.conditions);
2098
+ }
2099
+ }
2100
+ filter = await this.enforceTenantContextFilter(filter);
2101
+ filter = await resolveDynamicVariables(filter, this.accountability);
2102
+ // Check if record exists and user has permission
2103
+ const filterJoins = [];
2104
+ const whereClause = drizzleWhere(filter, {
2105
+ table: this.table,
2106
+ tableName: this.collection,
2107
+ schema: this.table,
2108
+ joins: filterJoins,
2109
+ forPermissionCheck: true
2110
+ });
2111
+ // Deduplicate filter joins by alias
2112
+ const uniqueJoins = [];
2113
+ const seenAliases = new Set();
2114
+ for (const join of filterJoins) {
2115
+ if (!seenAliases.has(join.alias)) {
2116
+ seenAliases.add(join.alias);
2117
+ uniqueJoins.push(join);
2118
+ }
2119
+ }
2120
+ filterJoins.length = 0;
2121
+ filterJoins.push(...uniqueJoins);
2122
+ let existingItems;
2123
+ if (filterJoins.length > 0) {
2124
+ // Use transaction for reads to prevent deadlocks and ensure consistency
2125
+ let query = transaction.select().from(this.table);
2126
+ filterJoins.forEach((join) => {
2127
+ const { table: joinTable, condition, type = 'left' } = join;
2128
+ const joinMethod = type === 'inner' ? 'innerJoin' : 'leftJoin';
2129
+ query = query[joinMethod](joinTable, condition);
2130
+ });
2131
+ existingItems = await query.where(whereClause).limit(1);
2132
+ }
2133
+ else {
2134
+ // Use transaction for reads to prevent deadlocks and ensure consistency
2135
+ existingItems = await transaction
2136
+ .select()
2137
+ .from(this.table)
2138
+ .where(whereClause)
2139
+ .limit(1);
2140
+ }
2141
+ if (!existingItems || existingItems.length === 0) {
2142
+ throw new APIError("Item not found or you don't have permission to delete it", 403);
2143
+ }
2144
+ const item = existingItems[0];
2145
+ // Handle related records cleanup based on onDelete settings
2146
+ await handleRelatedRecordsBeforeDelete(item, this, transaction);
2147
+ // Check if paranoid mode is enabled
2148
+ const isParanoid = schemaManager.isParanoid(this.collection);
2149
+ const forceDelete = options.force === true;
2150
+ let result;
2151
+ if (isParanoid && !forceDelete) {
2152
+ // Soft delete: Set deletedAt timestamp
2153
+ const userId = this.accountability?.user?.id;
2154
+ const softDeleteData = softDelete(userId ? String(userId) : undefined);
2155
+ result = await transaction
2156
+ .update(this.table)
2157
+ .set(softDeleteData)
2158
+ .where(eq(this.getPrimaryKeyColumn(), parsedId))
2159
+ .returning();
2160
+ if (!result || result.length === 0) {
2161
+ throw new APIError('Item not found or already deleted', 404);
2162
+ }
2163
+ }
2164
+ else {
2165
+ // Hard delete: Physically remove from database
2166
+ result = await transaction
2167
+ .delete(this.table)
2168
+ .where(eq(this.getPrimaryKeyColumn(), parsedId))
2169
+ .returning();
2170
+ if (!result || result.length === 0) {
2171
+ throw new APIError('Item not found or already deleted', 404);
2172
+ }
2173
+ }
2174
+ return {
2175
+ parsedId,
2176
+ document: item
2177
+ };
2178
+ }
2179
+ /**
2180
+ * Delete multiple records with transactional safety
2181
+ *
2182
+ * This method ensures that:
2183
+ * 1. All items are deleted within a single transaction
2184
+ * 2. If any deletion fails, all previous deletions are rolled back
2185
+ * 3. After hooks (which may have external side effects like emails, API calls)
2186
+ * are only executed AFTER the transaction is successfully committed
2187
+ *
2188
+ * @param ids - Array of item IDs to delete
2189
+ * @param options - Operation options
2190
+ * @returns Array of deleted item IDs
2191
+ */
2192
+ async deleteMany(ids, options = {}) {
2193
+ if (ids.length === 0) {
2194
+ return [];
2195
+ }
2196
+ // Create transaction if not provided
2197
+ const transaction = options.transaction || (await createTransaction());
2198
+ const shouldCommit = !options.transaction;
2199
+ // Store results for after hooks
2200
+ const results = [];
2201
+ try {
2202
+ // Phase 1: Execute all core delete operations within transaction
2203
+ for (const id of ids) {
2204
+ const result = await this.deleteOneCore(id, transaction, options);
2205
+ results.push(result);
2206
+ }
2207
+ // Phase 2: Commit transaction (if we created it)
2208
+ if (shouldCommit) {
2209
+ await transaction.commit();
2210
+ }
2211
+ // Phase 3: Execute after hooks AFTER successful commit
2212
+ for (const result of results) {
2213
+ // Create audit log
2214
+ await this.createAuditLog('delete', result.parsedId, {
2215
+ before: result.document,
2216
+ after: null
2217
+ }, options.transaction);
2218
+ // Execute after-delete hooks
2219
+ await hooksManager.executeHooks(this.collection, 'items.delete.after', this.accountability, { id: result.parsedId, document: result.document, transaction: options.transaction });
2220
+ }
2221
+ // Invalidate cache for this collection
2222
+ await this.invalidateCache();
2223
+ return results.map(r => r.parsedId);
2224
+ }
2225
+ catch (error) {
2226
+ console.error('Error in deleteMany:', error);
2227
+ if (shouldCommit) {
2228
+ try {
2229
+ await transaction.rollback();
2230
+ }
2231
+ catch (rollbackError) {
2232
+ if (rollbackError.message !== '__ROLLBACK__') {
2233
+ console.error('Error during rollback:', rollbackError);
2234
+ }
2235
+ }
2236
+ }
2237
+ throw error;
2238
+ }
2239
+ }
2240
+ /**
2241
+ * Delete a record by ID
2242
+ * Uses deleteOneCore for the transactional logic and executes after hooks after commit
2243
+ */
2244
+ async deleteOne(id, options = {}) {
2245
+ // Create transaction if not provided (matches Sequelize pattern)
2246
+ const transaction = options.transaction || (await createTransaction());
2247
+ const shouldCommit = !options.transaction;
2248
+ try {
2249
+ // Execute core delete logic within transaction
2250
+ const result = await this.deleteOneCore(id, transaction, options);
2251
+ // Commit transaction if we created it
2252
+ if (shouldCommit) {
2253
+ await transaction.commit();
2254
+ }
2255
+ // Create audit log for delete action (after commit)
2256
+ await this.createAuditLog('delete', result.parsedId, {
2257
+ before: result.document,
2258
+ after: null
2259
+ }, options.transaction);
2260
+ // Execute after-delete hooks (after commit to prevent side effects on rollback)
2261
+ await hooksManager.executeHooks(this.collection, 'items.delete.after', this.accountability, { id: result.parsedId, document: result.document, transaction: options.transaction });
2262
+ // Invalidate cache for this collection
2263
+ await this.invalidateCache();
2264
+ return result.parsedId;
2265
+ }
2266
+ catch (error) {
2267
+ console.error('Error in deleteOne:', error);
2268
+ // Rollback transaction if we created it
2269
+ if (shouldCommit) {
2270
+ try {
2271
+ await transaction.rollback();
2272
+ }
2273
+ catch (rollbackError) {
2274
+ if (rollbackError.message !== '__ROLLBACK__') {
2275
+ console.error('Error during rollback:', rollbackError);
2276
+ }
2277
+ }
2278
+ }
2279
+ throw error;
2280
+ }
2281
+ }
2282
+ /**
2283
+ * Alias for readByQuery
2284
+ */
2285
+ async list(query = {}, bypassPermissions = false) {
2286
+ return this.readByQuery(query, bypassPermissions);
2287
+ }
2288
+ /**
2289
+ * Alias for readOne
2290
+ */
2291
+ async read(id, query = {}, bypassPermissions = false) {
2292
+ return this.readOne(id, query, bypassPermissions);
2293
+ }
2294
+ /**
2295
+ * Alias for createOne
2296
+ */
2297
+ async create(data, options = {}) {
2298
+ return this.createOne(data, options);
2299
+ }
2300
+ /**
2301
+ * Alias for updateOne
2302
+ */
2303
+ async update(id, data, options = {}) {
2304
+ return this.updateOne(id, data, options);
2305
+ }
2306
+ /**
2307
+ * Alias for deleteOne
2308
+ */
2309
+ async delete(id, options = {}) {
2310
+ return this.deleteOne(id, options);
2311
+ }
2312
+ /**
2313
+ * Restore a soft-deleted record
2314
+ * Only works for collections with paranoid mode enabled
2315
+ */
2316
+ async restore(id, options = {}) {
2317
+ const parsedId = this.parseId(id);
2318
+ try {
2319
+ // Check if paranoid mode is enabled
2320
+ const isParanoid = schemaManager.isParanoid(this.collection);
2321
+ if (!isParanoid) {
2322
+ throw new APIError(`Collection ${this.collection} does not have soft delete enabled`, 400);
2323
+ }
2324
+ const isAdmin = await this.isAdministrator();
2325
+ // Check permission
2326
+ if (!options.bypassPermissions && !isAdmin) {
2327
+ const roleId = typeof this.accountability?.role === 'object' ? this.accountability.role.id : this.accountability?.role;
2328
+ const hasPermission = await permissionService.canAccess(roleId, this.collection, 'update' // Restore requires update permission
2329
+ );
2330
+ if (!hasPermission) {
2331
+ throw new APIError("You don't have permission to restore this item", 403);
2332
+ }
2333
+ }
2334
+ // Build filter for existing record check (include soft-deleted)
2335
+ let filter = {
2336
+ [`${this.collection}.${this.primaryKey}`]: parsedId,
2337
+ [`${this.collection}.deletedAt`]: { _is_not_null: true } // Only restore soft-deleted items
2338
+ };
2339
+ filter = await this.enforceTenantContextFilter(filter);
2340
+ // Check if record exists and is soft-deleted
2341
+ const whereClause = drizzleWhere(filter, {
2342
+ table: this.table,
2343
+ tableName: this.collection,
2344
+ schema: this.table
2345
+ });
2346
+ const existingItems = await db
2347
+ .select()
2348
+ .from(this.table)
2349
+ .where(whereClause)
2350
+ .limit(1);
2351
+ if (!existingItems || existingItems.length === 0) {
2352
+ throw new APIError("Item not found or not soft-deleted", 404);
2353
+ }
2354
+ // Restore: Set deletedAt to null
2355
+ const restoreData = restore();
2356
+ const result = await db
2357
+ .update(this.table)
2358
+ .set(restoreData)
2359
+ .where(eq(this.getPrimaryKeyColumn(), parsedId))
2360
+ .returning();
2361
+ if (!result || result.length === 0) {
2362
+ throw new APIError('Failed to restore item', 500);
2363
+ }
2364
+ return parsedId;
2365
+ }
2366
+ catch (error) {
2367
+ console.error('Error in restore:', error);
2368
+ throw error;
2369
+ }
2370
+ }
2371
+ /**
2372
+ * Create an audit log entry for create/update/delete operations
2373
+ * @param action - The action performed (create, update, delete)
2374
+ * @param entityId - The ID of the entity
2375
+ * @param changes - The changes made (before and after states)
2376
+ * @param transaction - Optional transaction to use
2377
+ */
2378
+ async createAuditLog(action, entityId, changes, transaction) {
2379
+ // Collections to exclude from audit logging
2380
+ const excludeCollections = ['baasix_AuditLog', 'baasix_Sessions'];
2381
+ // Skip audit log creation for excluded collections
2382
+ if (excludeCollections.includes(this.collection)) {
2383
+ return;
2384
+ }
2385
+ try {
2386
+ // Get special handling for baasix_SchemaDefinition - use collectionName as ID
2387
+ let auditEntityId = entityId;
2388
+ if (this.collection === 'baasix_SchemaDefinition' && changes.after?.collectionName) {
2389
+ auditEntityId = changes.after.collectionName;
2390
+ }
2391
+ // Serialize changes to JSON to avoid Postgres type issues
2392
+ // Drizzle expects JSON fields to be properly serialized
2393
+ const serializedChanges = {
2394
+ before: changes.before ? JSON.parse(JSON.stringify(changes.before)) : null,
2395
+ after: changes.after ? JSON.parse(JSON.stringify(changes.after)) : null
2396
+ };
2397
+ // Prepare audit log data
2398
+ const auditLogData = {
2399
+ type: 'data',
2400
+ entity: this.collection,
2401
+ entityId: String(auditEntityId),
2402
+ action: action,
2403
+ changes: serializedChanges,
2404
+ userId: this.accountability?.user?.id || null,
2405
+ ipaddress: this.accountability?.ipaddress || null,
2406
+ };
2407
+ // Add tenant_Id if multi-tenant is enabled
2408
+ const tenantId = this.tenant || this.accountability?.tenant;
2409
+ if (tenantId) {
2410
+ auditLogData.tenant_Id = tenantId;
2411
+ }
2412
+ // Create audit log entry using ItemsService to avoid circular dependency
2413
+ const auditLogTable = schemaManager.getTable('baasix_AuditLog');
2414
+ if (!auditLogTable) {
2415
+ console.warn('[AuditLog] baasix_AuditLog table not found, skipping audit log creation');
2416
+ return;
2417
+ }
2418
+ // Use transaction if provided, otherwise use db directly
2419
+ const dbClient = transaction || db;
2420
+ await dbClient
2421
+ .insert(auditLogTable)
2422
+ .values(auditLogData);
2423
+ }
2424
+ catch (error) {
2425
+ // Log error but don't fail the main operation
2426
+ console.error('[AuditLog] Failed to create audit log:', error.message);
2427
+ }
2428
+ }
2429
+ /**
2430
+ * Convert date string fields to Date objects for DateTime/Timestamp fields
2431
+ * This is needed because Drizzle expects Date objects for timestamp columns
2432
+ * Note: Date type (date-only) expects strings, not Date objects
2433
+ */
2434
+ async convertDateFields(data, schemaDefinition) {
2435
+ if (!schemaDefinition || !schemaDefinition.fields) {
2436
+ return;
2437
+ }
2438
+ const fields = schemaDefinition.fields;
2439
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
2440
+ const fieldConfig = fieldDef;
2441
+ // Only convert DateTime/DateTime_NO_TZ/Timestamp to Date objects
2442
+ // Do NOT convert Date type (date-only) - it expects strings
2443
+ if ((fieldConfig.type === 'DateTime' || fieldConfig.type === 'DateTime_NO_TZ' || fieldConfig.type === 'Timestamp') &&
2444
+ data[fieldName] !== undefined &&
2445
+ data[fieldName] !== null) {
2446
+ // Convert string to Date object if it's not already a Date
2447
+ if (typeof data[fieldName] === 'string') {
2448
+ const dateValue = new Date(data[fieldName]);
2449
+ if (!isNaN(dateValue.getTime())) {
2450
+ data[fieldName] = dateValue;
2451
+ }
2452
+ }
2453
+ }
2454
+ }
2455
+ }
2456
+ }
2457
+ export default ItemsService;
2458
+ //# sourceMappingURL=ItemsService.js.map