@warlock.js/cascade 4.0.135 → 4.0.137

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 (339) hide show
  1. package/cjs/context/database-transaction-context.d.ts +4 -0
  2. package/cjs/context/database-transaction-context.d.ts.map +1 -1
  3. package/cjs/context/database-transaction-context.js +6 -0
  4. package/cjs/context/database-transaction-context.js.map +1 -1
  5. package/cjs/contracts/database-driver.contract.d.ts +106 -4
  6. package/cjs/contracts/database-driver.contract.d.ts.map +1 -1
  7. package/cjs/contracts/migration-driver.contract.d.ts +49 -1
  8. package/cjs/contracts/migration-driver.contract.d.ts.map +1 -1
  9. package/cjs/contracts/query-builder.contract.d.ts +76 -4
  10. package/cjs/contracts/query-builder.contract.d.ts.map +1 -1
  11. package/cjs/data-source/data-source.d.ts +40 -1
  12. package/cjs/data-source/data-source.d.ts.map +1 -1
  13. package/cjs/data-source/data-source.js +6 -0
  14. package/cjs/data-source/data-source.js.map +1 -1
  15. package/cjs/database-dirty-tracker.d.ts +21 -22
  16. package/cjs/database-dirty-tracker.d.ts.map +1 -1
  17. package/cjs/database-dirty-tracker.js +14 -17
  18. package/cjs/database-dirty-tracker.js.map +1 -1
  19. package/cjs/drivers/mongodb/mongodb-driver.d.ts +29 -1
  20. package/cjs/drivers/mongodb/mongodb-driver.d.ts.map +1 -1
  21. package/cjs/drivers/mongodb/mongodb-driver.js +116 -4
  22. package/cjs/drivers/mongodb/mongodb-driver.js.map +1 -1
  23. package/cjs/drivers/mongodb/mongodb-migration-driver.d.ts +22 -1
  24. package/cjs/drivers/mongodb/mongodb-migration-driver.d.ts.map +1 -1
  25. package/cjs/drivers/mongodb/mongodb-migration-driver.js +27 -0
  26. package/cjs/drivers/mongodb/mongodb-migration-driver.js.map +1 -1
  27. package/cjs/drivers/mongodb/mongodb-query-builder.d.ts +41 -9
  28. package/cjs/drivers/mongodb/mongodb-query-builder.d.ts.map +1 -1
  29. package/cjs/drivers/mongodb/mongodb-query-builder.js +60 -18
  30. package/cjs/drivers/mongodb/mongodb-query-builder.js.map +1 -1
  31. package/cjs/drivers/mongodb/types.d.ts +1 -1
  32. package/cjs/drivers/mongodb/types.d.ts.map +1 -1
  33. package/cjs/drivers/postgres/postgres-dialect.d.ts +1 -0
  34. package/cjs/drivers/postgres/postgres-dialect.d.ts.map +1 -1
  35. package/cjs/drivers/postgres/postgres-dialect.js +24 -1
  36. package/cjs/drivers/postgres/postgres-dialect.js.map +1 -1
  37. package/cjs/drivers/postgres/postgres-driver.d.ts +24 -3
  38. package/cjs/drivers/postgres/postgres-driver.d.ts.map +1 -1
  39. package/cjs/drivers/postgres/postgres-driver.js +101 -8
  40. package/cjs/drivers/postgres/postgres-driver.js.map +1 -1
  41. package/cjs/drivers/postgres/postgres-migration-driver.d.ts +30 -1
  42. package/cjs/drivers/postgres/postgres-migration-driver.d.ts.map +1 -1
  43. package/cjs/drivers/postgres/postgres-migration-driver.js +61 -2
  44. package/cjs/drivers/postgres/postgres-migration-driver.js.map +1 -1
  45. package/cjs/drivers/postgres/postgres-query-builder.d.ts +152 -722
  46. package/cjs/drivers/postgres/postgres-query-builder.d.ts.map +1 -1
  47. package/cjs/drivers/postgres/postgres-query-builder.js +592 -1459
  48. package/cjs/drivers/postgres/postgres-query-builder.js.map +1 -1
  49. package/cjs/drivers/postgres/postgres-query-parser.d.ts +34 -14
  50. package/cjs/drivers/postgres/postgres-query-parser.d.ts.map +1 -1
  51. package/cjs/drivers/postgres/postgres-query-parser.js +197 -39
  52. package/cjs/drivers/postgres/postgres-query-parser.js.map +1 -1
  53. package/cjs/drivers/sql/sql-dialect.contract.d.ts +1 -0
  54. package/cjs/drivers/sql/sql-dialect.contract.d.ts.map +1 -1
  55. package/cjs/errors/transaction-rollback.error.d.ts +20 -0
  56. package/cjs/errors/transaction-rollback.error.d.ts.map +1 -0
  57. package/cjs/errors/transaction-rollback.error.js +27 -0
  58. package/cjs/errors/transaction-rollback.error.js.map +1 -0
  59. package/cjs/events/model-events.d.ts +1 -1
  60. package/cjs/events/model-events.d.ts.map +1 -1
  61. package/cjs/index.d.ts +1 -0
  62. package/cjs/index.d.ts.map +1 -1
  63. package/cjs/index.js +1 -1
  64. package/cjs/migration/column-builder.d.ts +4 -0
  65. package/cjs/migration/column-builder.d.ts.map +1 -1
  66. package/cjs/migration/column-builder.js +23 -6
  67. package/cjs/migration/column-builder.js.map +1 -1
  68. package/cjs/migration/migration-runner.d.ts.map +1 -1
  69. package/cjs/migration/migration-runner.js +137 -23
  70. package/cjs/migration/migration-runner.js.map +1 -1
  71. package/cjs/migration/migration.d.ts +209 -35
  72. package/cjs/migration/migration.d.ts.map +1 -1
  73. package/cjs/migration/migration.js +192 -27
  74. package/cjs/migration/migration.js.map +1 -1
  75. package/cjs/model/methods/accessor-methods.d.ts +13 -0
  76. package/cjs/model/methods/accessor-methods.d.ts.map +1 -0
  77. package/cjs/model/methods/accessor-methods.js +51 -0
  78. package/cjs/model/methods/accessor-methods.js.map +1 -0
  79. package/cjs/model/methods/delete-methods.d.ts +10 -0
  80. package/cjs/model/methods/delete-methods.d.ts.map +1 -0
  81. package/cjs/model/methods/delete-methods.js +10 -0
  82. package/cjs/model/methods/delete-methods.js.map +1 -0
  83. package/cjs/model/methods/dirty-methods.d.ts +10 -0
  84. package/cjs/model/methods/dirty-methods.d.ts.map +1 -0
  85. package/cjs/model/methods/dirty-methods.js +15 -0
  86. package/cjs/model/methods/dirty-methods.js.map +1 -0
  87. package/cjs/model/methods/hydration-methods.d.ts +10 -0
  88. package/cjs/model/methods/hydration-methods.d.ts.map +1 -0
  89. package/cjs/model/methods/hydration-methods.js +57 -0
  90. package/cjs/model/methods/hydration-methods.js.map +1 -0
  91. package/cjs/model/methods/instance-event-methods.d.ts +7 -0
  92. package/cjs/model/methods/instance-event-methods.d.ts.map +1 -0
  93. package/cjs/model/methods/instance-event-methods.js +15 -0
  94. package/cjs/model/methods/instance-event-methods.js.map +1 -0
  95. package/cjs/model/methods/meta-methods.d.ts +7 -0
  96. package/cjs/model/methods/meta-methods.d.ts.map +1 -0
  97. package/cjs/model/methods/meta-methods.js +78 -0
  98. package/cjs/model/methods/meta-methods.js.map +1 -0
  99. package/cjs/model/methods/query-methods.d.ts +24 -0
  100. package/cjs/model/methods/query-methods.d.ts.map +1 -0
  101. package/cjs/model/methods/query-methods.js +161 -0
  102. package/cjs/model/methods/query-methods.js.map +1 -0
  103. package/cjs/model/methods/restore-methods.d.ts +10 -0
  104. package/cjs/model/methods/restore-methods.d.ts.map +1 -0
  105. package/cjs/model/methods/restore-methods.js +13 -0
  106. package/cjs/model/methods/restore-methods.js.map +1 -0
  107. package/cjs/model/methods/scope-methods.d.ts +7 -0
  108. package/cjs/model/methods/scope-methods.d.ts.map +1 -0
  109. package/cjs/model/methods/scope-methods.js +15 -0
  110. package/cjs/model/methods/scope-methods.js.map +1 -0
  111. package/cjs/model/methods/serialization-methods.d.ts +3 -0
  112. package/cjs/model/methods/serialization-methods.d.ts.map +1 -0
  113. package/cjs/model/methods/serialization-methods.js +27 -0
  114. package/cjs/model/methods/serialization-methods.js.map +1 -0
  115. package/cjs/model/methods/static-event-methods.d.ts +9 -0
  116. package/cjs/model/methods/static-event-methods.d.ts.map +1 -0
  117. package/cjs/model/methods/static-event-methods.js +29 -0
  118. package/cjs/model/methods/static-event-methods.js.map +1 -0
  119. package/cjs/model/methods/write-methods.d.ts +10 -0
  120. package/cjs/model/methods/write-methods.d.ts.map +1 -0
  121. package/cjs/model/methods/write-methods.js +52 -0
  122. package/cjs/model/methods/write-methods.js.map +1 -0
  123. package/cjs/model/model.d.ts +89 -58
  124. package/cjs/model/model.d.ts.map +1 -1
  125. package/cjs/model/model.js +166 -424
  126. package/cjs/model/model.js.map +1 -1
  127. package/cjs/model/model.types.d.ts +44 -0
  128. package/cjs/model/model.types.d.ts.map +1 -0
  129. package/cjs/model/register-model.d.ts +3 -3
  130. package/cjs/model/register-model.d.ts.map +1 -1
  131. package/cjs/query-builder/query-builder.d.ts +556 -0
  132. package/cjs/query-builder/query-builder.d.ts.map +1 -0
  133. package/cjs/query-builder/query-builder.js +1070 -0
  134. package/cjs/query-builder/query-builder.js.map +1 -0
  135. package/cjs/relations/helpers.d.ts.map +1 -1
  136. package/cjs/relations/helpers.js +4 -0
  137. package/cjs/relations/helpers.js.map +1 -1
  138. package/cjs/relations/index.d.ts +2 -0
  139. package/cjs/relations/index.d.ts.map +1 -1
  140. package/cjs/relations/relation-hydrator.d.ts +68 -0
  141. package/cjs/relations/relation-hydrator.d.ts.map +1 -0
  142. package/cjs/relations/relation-hydrator.js +81 -0
  143. package/cjs/relations/relation-hydrator.js.map +1 -0
  144. package/cjs/relations/relation-loader.js +1 -1
  145. package/cjs/relations/relation-loader.js.map +1 -1
  146. package/cjs/relations/types.d.ts +26 -0
  147. package/cjs/relations/types.d.ts.map +1 -1
  148. package/cjs/sql-database-dirty-tracker.d.ts +13 -0
  149. package/cjs/sql-database-dirty-tracker.d.ts.map +1 -0
  150. package/cjs/sql-database-dirty-tracker.js +14 -0
  151. package/cjs/sql-database-dirty-tracker.js.map +1 -0
  152. package/cjs/types.d.ts +59 -0
  153. package/cjs/types.d.ts.map +1 -1
  154. package/cjs/utils/connect-to-database.d.ts +50 -1
  155. package/cjs/utils/connect-to-database.d.ts.map +1 -1
  156. package/cjs/utils/connect-to-database.js +14 -1
  157. package/cjs/utils/connect-to-database.js.map +1 -1
  158. package/cjs/utils/database-writer.utils.d.ts +1 -1
  159. package/cjs/utils/database-writer.utils.d.ts.map +1 -1
  160. package/cjs/utils/is-valid-date-value.d.ts +5 -0
  161. package/cjs/utils/is-valid-date-value.d.ts.map +1 -0
  162. package/cjs/utils/is-valid-date-value.js +25 -0
  163. package/cjs/utils/is-valid-date-value.js.map +1 -0
  164. package/cjs/utils/once-connected.d.ts.map +1 -1
  165. package/cjs/utils/once-connected.js +4 -5
  166. package/cjs/utils/once-connected.js.map +1 -1
  167. package/cjs/writer/database-writer.d.ts.map +1 -1
  168. package/cjs/writer/database-writer.js +7 -6
  169. package/cjs/writer/database-writer.js.map +1 -1
  170. package/esm/context/database-transaction-context.d.ts +4 -0
  171. package/esm/context/database-transaction-context.d.ts.map +1 -1
  172. package/esm/context/database-transaction-context.js +6 -0
  173. package/esm/context/database-transaction-context.js.map +1 -1
  174. package/esm/contracts/database-driver.contract.d.ts +106 -4
  175. package/esm/contracts/database-driver.contract.d.ts.map +1 -1
  176. package/esm/contracts/migration-driver.contract.d.ts +49 -1
  177. package/esm/contracts/migration-driver.contract.d.ts.map +1 -1
  178. package/esm/contracts/query-builder.contract.d.ts +76 -4
  179. package/esm/contracts/query-builder.contract.d.ts.map +1 -1
  180. package/esm/data-source/data-source.d.ts +40 -1
  181. package/esm/data-source/data-source.d.ts.map +1 -1
  182. package/esm/data-source/data-source.js +6 -0
  183. package/esm/data-source/data-source.js.map +1 -1
  184. package/esm/database-dirty-tracker.d.ts +21 -22
  185. package/esm/database-dirty-tracker.d.ts.map +1 -1
  186. package/esm/database-dirty-tracker.js +14 -17
  187. package/esm/database-dirty-tracker.js.map +1 -1
  188. package/esm/drivers/mongodb/mongodb-driver.d.ts +29 -1
  189. package/esm/drivers/mongodb/mongodb-driver.d.ts.map +1 -1
  190. package/esm/drivers/mongodb/mongodb-driver.js +116 -4
  191. package/esm/drivers/mongodb/mongodb-driver.js.map +1 -1
  192. package/esm/drivers/mongodb/mongodb-migration-driver.d.ts +22 -1
  193. package/esm/drivers/mongodb/mongodb-migration-driver.d.ts.map +1 -1
  194. package/esm/drivers/mongodb/mongodb-migration-driver.js +27 -0
  195. package/esm/drivers/mongodb/mongodb-migration-driver.js.map +1 -1
  196. package/esm/drivers/mongodb/mongodb-query-builder.d.ts +41 -9
  197. package/esm/drivers/mongodb/mongodb-query-builder.d.ts.map +1 -1
  198. package/esm/drivers/mongodb/mongodb-query-builder.js +60 -18
  199. package/esm/drivers/mongodb/mongodb-query-builder.js.map +1 -1
  200. package/esm/drivers/mongodb/types.d.ts +1 -1
  201. package/esm/drivers/mongodb/types.d.ts.map +1 -1
  202. package/esm/drivers/postgres/postgres-dialect.d.ts +1 -0
  203. package/esm/drivers/postgres/postgres-dialect.d.ts.map +1 -1
  204. package/esm/drivers/postgres/postgres-dialect.js +24 -1
  205. package/esm/drivers/postgres/postgres-dialect.js.map +1 -1
  206. package/esm/drivers/postgres/postgres-driver.d.ts +24 -3
  207. package/esm/drivers/postgres/postgres-driver.d.ts.map +1 -1
  208. package/esm/drivers/postgres/postgres-driver.js +101 -8
  209. package/esm/drivers/postgres/postgres-driver.js.map +1 -1
  210. package/esm/drivers/postgres/postgres-migration-driver.d.ts +30 -1
  211. package/esm/drivers/postgres/postgres-migration-driver.d.ts.map +1 -1
  212. package/esm/drivers/postgres/postgres-migration-driver.js +61 -2
  213. package/esm/drivers/postgres/postgres-migration-driver.js.map +1 -1
  214. package/esm/drivers/postgres/postgres-query-builder.d.ts +152 -722
  215. package/esm/drivers/postgres/postgres-query-builder.d.ts.map +1 -1
  216. package/esm/drivers/postgres/postgres-query-builder.js +592 -1459
  217. package/esm/drivers/postgres/postgres-query-builder.js.map +1 -1
  218. package/esm/drivers/postgres/postgres-query-parser.d.ts +34 -14
  219. package/esm/drivers/postgres/postgres-query-parser.d.ts.map +1 -1
  220. package/esm/drivers/postgres/postgres-query-parser.js +197 -39
  221. package/esm/drivers/postgres/postgres-query-parser.js.map +1 -1
  222. package/esm/drivers/sql/sql-dialect.contract.d.ts +1 -0
  223. package/esm/drivers/sql/sql-dialect.contract.d.ts.map +1 -1
  224. package/esm/errors/transaction-rollback.error.d.ts +20 -0
  225. package/esm/errors/transaction-rollback.error.d.ts.map +1 -0
  226. package/esm/errors/transaction-rollback.error.js +27 -0
  227. package/esm/errors/transaction-rollback.error.js.map +1 -0
  228. package/esm/events/model-events.d.ts +1 -1
  229. package/esm/events/model-events.d.ts.map +1 -1
  230. package/esm/index.d.ts +1 -0
  231. package/esm/index.d.ts.map +1 -1
  232. package/esm/index.js +1 -1
  233. package/esm/migration/column-builder.d.ts +4 -0
  234. package/esm/migration/column-builder.d.ts.map +1 -1
  235. package/esm/migration/column-builder.js +23 -6
  236. package/esm/migration/column-builder.js.map +1 -1
  237. package/esm/migration/migration-runner.d.ts.map +1 -1
  238. package/esm/migration/migration-runner.js +137 -23
  239. package/esm/migration/migration-runner.js.map +1 -1
  240. package/esm/migration/migration.d.ts +209 -35
  241. package/esm/migration/migration.d.ts.map +1 -1
  242. package/esm/migration/migration.js +192 -27
  243. package/esm/migration/migration.js.map +1 -1
  244. package/esm/model/methods/accessor-methods.d.ts +13 -0
  245. package/esm/model/methods/accessor-methods.d.ts.map +1 -0
  246. package/esm/model/methods/accessor-methods.js +51 -0
  247. package/esm/model/methods/accessor-methods.js.map +1 -0
  248. package/esm/model/methods/delete-methods.d.ts +10 -0
  249. package/esm/model/methods/delete-methods.d.ts.map +1 -0
  250. package/esm/model/methods/delete-methods.js +10 -0
  251. package/esm/model/methods/delete-methods.js.map +1 -0
  252. package/esm/model/methods/dirty-methods.d.ts +10 -0
  253. package/esm/model/methods/dirty-methods.d.ts.map +1 -0
  254. package/esm/model/methods/dirty-methods.js +15 -0
  255. package/esm/model/methods/dirty-methods.js.map +1 -0
  256. package/esm/model/methods/hydration-methods.d.ts +10 -0
  257. package/esm/model/methods/hydration-methods.d.ts.map +1 -0
  258. package/esm/model/methods/hydration-methods.js +57 -0
  259. package/esm/model/methods/hydration-methods.js.map +1 -0
  260. package/esm/model/methods/instance-event-methods.d.ts +7 -0
  261. package/esm/model/methods/instance-event-methods.d.ts.map +1 -0
  262. package/esm/model/methods/instance-event-methods.js +15 -0
  263. package/esm/model/methods/instance-event-methods.js.map +1 -0
  264. package/esm/model/methods/meta-methods.d.ts +7 -0
  265. package/esm/model/methods/meta-methods.d.ts.map +1 -0
  266. package/esm/model/methods/meta-methods.js +78 -0
  267. package/esm/model/methods/meta-methods.js.map +1 -0
  268. package/esm/model/methods/query-methods.d.ts +24 -0
  269. package/esm/model/methods/query-methods.d.ts.map +1 -0
  270. package/esm/model/methods/query-methods.js +161 -0
  271. package/esm/model/methods/query-methods.js.map +1 -0
  272. package/esm/model/methods/restore-methods.d.ts +10 -0
  273. package/esm/model/methods/restore-methods.d.ts.map +1 -0
  274. package/esm/model/methods/restore-methods.js +13 -0
  275. package/esm/model/methods/restore-methods.js.map +1 -0
  276. package/esm/model/methods/scope-methods.d.ts +7 -0
  277. package/esm/model/methods/scope-methods.d.ts.map +1 -0
  278. package/esm/model/methods/scope-methods.js +15 -0
  279. package/esm/model/methods/scope-methods.js.map +1 -0
  280. package/esm/model/methods/serialization-methods.d.ts +3 -0
  281. package/esm/model/methods/serialization-methods.d.ts.map +1 -0
  282. package/esm/model/methods/serialization-methods.js +27 -0
  283. package/esm/model/methods/serialization-methods.js.map +1 -0
  284. package/esm/model/methods/static-event-methods.d.ts +9 -0
  285. package/esm/model/methods/static-event-methods.d.ts.map +1 -0
  286. package/esm/model/methods/static-event-methods.js +29 -0
  287. package/esm/model/methods/static-event-methods.js.map +1 -0
  288. package/esm/model/methods/write-methods.d.ts +10 -0
  289. package/esm/model/methods/write-methods.d.ts.map +1 -0
  290. package/esm/model/methods/write-methods.js +52 -0
  291. package/esm/model/methods/write-methods.js.map +1 -0
  292. package/esm/model/model.d.ts +89 -58
  293. package/esm/model/model.d.ts.map +1 -1
  294. package/esm/model/model.js +166 -424
  295. package/esm/model/model.js.map +1 -1
  296. package/esm/model/model.types.d.ts +44 -0
  297. package/esm/model/model.types.d.ts.map +1 -0
  298. package/esm/model/register-model.d.ts +3 -3
  299. package/esm/model/register-model.d.ts.map +1 -1
  300. package/esm/query-builder/query-builder.d.ts +556 -0
  301. package/esm/query-builder/query-builder.d.ts.map +1 -0
  302. package/esm/query-builder/query-builder.js +1070 -0
  303. package/esm/query-builder/query-builder.js.map +1 -0
  304. package/esm/relations/helpers.d.ts.map +1 -1
  305. package/esm/relations/helpers.js +4 -0
  306. package/esm/relations/helpers.js.map +1 -1
  307. package/esm/relations/index.d.ts +2 -0
  308. package/esm/relations/index.d.ts.map +1 -1
  309. package/esm/relations/relation-hydrator.d.ts +68 -0
  310. package/esm/relations/relation-hydrator.d.ts.map +1 -0
  311. package/esm/relations/relation-hydrator.js +81 -0
  312. package/esm/relations/relation-hydrator.js.map +1 -0
  313. package/esm/relations/relation-loader.js +1 -1
  314. package/esm/relations/relation-loader.js.map +1 -1
  315. package/esm/relations/types.d.ts +26 -0
  316. package/esm/relations/types.d.ts.map +1 -1
  317. package/esm/sql-database-dirty-tracker.d.ts +13 -0
  318. package/esm/sql-database-dirty-tracker.d.ts.map +1 -0
  319. package/esm/sql-database-dirty-tracker.js +14 -0
  320. package/esm/sql-database-dirty-tracker.js.map +1 -0
  321. package/esm/types.d.ts +59 -0
  322. package/esm/types.d.ts.map +1 -1
  323. package/esm/utils/connect-to-database.d.ts +50 -1
  324. package/esm/utils/connect-to-database.d.ts.map +1 -1
  325. package/esm/utils/connect-to-database.js +14 -1
  326. package/esm/utils/connect-to-database.js.map +1 -1
  327. package/esm/utils/database-writer.utils.d.ts +1 -1
  328. package/esm/utils/database-writer.utils.d.ts.map +1 -1
  329. package/esm/utils/is-valid-date-value.d.ts +5 -0
  330. package/esm/utils/is-valid-date-value.d.ts.map +1 -0
  331. package/esm/utils/is-valid-date-value.js +25 -0
  332. package/esm/utils/is-valid-date-value.js.map +1 -0
  333. package/esm/utils/once-connected.d.ts.map +1 -1
  334. package/esm/utils/once-connected.js +4 -5
  335. package/esm/utils/once-connected.js.map +1 -1
  336. package/esm/writer/database-writer.d.ts.map +1 -1
  337. package/esm/writer/database-writer.js +7 -6
  338. package/esm/writer/database-writer.js.map +1 -1
  339. package/package.json +4 -4
@@ -1,155 +1,193 @@
1
- import {dataSourceRegistry}from'../../data-source/data-source-registry.js';import {getModelFromRegistry}from'../../model/register-model.js';import {PostgresQueryParser}from'./postgres-query-parser.js';/**
1
+ import {dataSourceRegistry}from'../../data-source/data-source-registry.js';import {getModelFromRegistry,resolveModelClass}from'../../model/register-model.js';import {QueryBuilder}from'../../query-builder/query-builder.js';import {PostgresQueryParser}from'./postgres-query-parser.js';/**
2
2
  * PostgreSQL Query Builder
3
3
  *
4
- * Implements the QueryBuilderContract for PostgreSQL databases.
5
- * Provides a fluent API for building SQL queries with proper
6
- * parameter handling and type safety.
4
+ * Extends the pure QueryBuilder base with PostgreSQL-specific execution,
5
+ * SQL generation, relation hydration, and scope management.
7
6
  *
8
7
  * @module cascade/drivers/postgres
9
8
  */
9
+ // ============================================================================
10
+ // HELPER
11
+ // ============================================================================
12
+ /**
13
+ * Cast an Op[] to PostgresParserOperation[] — the shapes are compatible since
14
+ * both have `type: string` and `data: Record<string, unknown>`.
15
+ */
16
+ function toParserOps(ops) {
17
+ return ops;
18
+ }
19
+ // ============================================================================
20
+ // POSTGRES QUERY BUILDER
21
+ // ============================================================================
10
22
  /**
11
23
  * PostgreSQL Query Builder.
12
24
  *
13
- * Implements the Cascade QueryBuilderContract for PostgreSQL.
14
- * Collects query operations and delegates to PostgresQueryParser
15
- * for SQL generation.
25
+ * Collects query operations (via the base class) and delegates SQL generation
26
+ * to `PostgresQueryParser`. Owns execution, hydration, and relation loading.
16
27
  *
17
28
  * @example
18
29
  * ```typescript
19
- * const users = await queryBuilder('users')
20
- * .select(['id', 'name', 'email'])
21
- * .where('status', 'active')
22
- * .where('age', '>', 18)
23
- * .orderBy('createdAt', 'desc')
30
+ * const users = await User.query()
31
+ * .select(["id", "name", "email"])
32
+ * .where("status", "active")
33
+ * .orderBy("createdAt", "desc")
24
34
  * .limit(10)
25
35
  * .get();
26
36
  * ```
27
37
  */
28
- class PostgresQueryBuilder {
38
+ class PostgresQueryBuilder extends QueryBuilder {
29
39
  table;
30
- /**
31
- * Collected operations to be parsed into SQL.
32
- */
33
- operations = [];
34
- /**
35
- * Data source instance.
36
- */
40
+ // ──────────────────────────────────────────────────────────────
41
+ // POSTGRES-SPECIFIC STATE
42
+ // ──────────────────────────────────────────────────────────────
43
+ /** Data source backing this builder. */
37
44
  dataSource;
38
- /**
39
- * Hydrate callback for transforming results.
40
- */
45
+ /** Hydration callback for transforming result rows into model instances. */
41
46
  hydrateCallback;
42
- /**
43
- * Callback invoked before query execution.
44
- */
47
+ /** Invoked before query execution. */
45
48
  fetchingCallback;
46
- /**
47
- * Callback invoked after records fetched but before hydration.
48
- */
49
+ /** Invoked after fetch but before hydration. */
49
50
  hydratingCallback;
50
- /**
51
- * Callback invoked after records fetched and hydrated.
52
- */
51
+ /** Invoked after fetch and hydration. */
53
52
  fetchedCallback;
54
53
  /**
55
- * Pending global scopes.
56
- */
57
- pendingGlobalScopes;
58
- /**
59
- * Available local scopes.
54
+ * Map of relations registered via `joinWith()`.
55
+ * Keyed by dot-notation path (e.g. "organizationAiModel.aiModel").
60
56
  */
61
- availableLocalScopes;
62
- /**
63
- * Disabled global scope names.
64
- */
65
- disabledGlobalScopes = new Set();
66
- /**
67
- * Whether scopes have been applied.
68
- */
69
- scopesApplied = false;
57
+ joinRelations = new Map();
58
+ // ──────────────────────────────────────────────────────────────
59
+ // CONSTRUCTOR
60
+ // ──────────────────────────────────────────────────────────────
70
61
  /**
71
- * Create a new query builder.
72
- *
73
62
  * @param table - Target table name
74
- * @param dataSource - Optional data source (uses default if not provided)
63
+ * @param dataSource - Optional (uses default data source from registry if omitted)
75
64
  */
76
65
  constructor(table, dataSource) {
66
+ super();
77
67
  this.table = table;
78
68
  this.dataSource = dataSource ?? dataSourceRegistry.get();
79
69
  }
80
- /**
81
- * Get the PostgreSQL driver instance.
82
- */
70
+ // ──────────────────────────────────────────────────────────────
71
+ // DRIVER
72
+ // ──────────────────────────────────────────────────────────────
83
73
  get driver() {
84
74
  return this.dataSource.driver;
85
75
  }
86
- /**
87
- * Add an operation to the operations list.
88
- *
89
- * @param type - Operation type
90
- * @param data - Operation data
91
- */
92
- addOperation(type, data) {
93
- this.operations.push({ type, data });
94
- }
95
- /**
96
- * Clone this query builder with all current operations.
97
- *
98
- * @returns New query builder instance
99
- */
76
+ // ──────────────────────────────────────────────────────────────
77
+ // CLONE
78
+ // ──────────────────────────────────────────────────────────────
100
79
  clone() {
101
80
  const cloned = new PostgresQueryBuilder(this.table, this.dataSource);
81
+ // Copy base-class state
102
82
  cloned.operations = [...this.operations];
103
- cloned.hydrateCallback = this.hydrateCallback;
104
83
  cloned.pendingGlobalScopes = this.pendingGlobalScopes;
105
84
  cloned.availableLocalScopes = this.availableLocalScopes;
106
85
  cloned.disabledGlobalScopes = new Set(this.disabledGlobalScopes);
107
86
  cloned.scopesApplied = this.scopesApplied;
87
+ cloned.eagerLoadRelations = new Map(this.eagerLoadRelations);
88
+ cloned.countRelations = [...this.countRelations];
89
+ cloned.relationDefinitions = this.relationDefinitions;
90
+ cloned.modelClass = this.modelClass;
91
+ // Copy PG-specific state
92
+ cloned.hydrateCallback = this.hydrateCallback;
93
+ cloned.joinRelations = new Map(this.joinRelations);
108
94
  return cloned;
109
95
  }
110
96
  // ============================================================================
111
- // HYDRATION
97
+ // PG-SPECIFIC FLUENT METHODS
112
98
  // ============================================================================
113
99
  /**
114
- * Set a hydration callback to transform each result row.
100
+ * Native-query escape hatch. Passes `operations[]` to the callback for
101
+ * direct manipulation. Use sparingly — only when fluent API is insufficient.
115
102
  *
116
- * @param callback - Transform function
117
- * @returns This builder for chaining
103
+ * @example
104
+ * q.raw(ops => ops.push({ type: "whereRaw", data: { expression: "1=1" } }))
118
105
  */
119
- hydrate(callback) {
120
- this.hydrateCallback = callback;
106
+ raw(callback) {
107
+ callback(this.operations);
121
108
  return this;
122
109
  }
123
110
  /**
124
- * Register callback invoked before query execution.
111
+ * Record a DISTINCT flag AND auto-select the field(s).
112
+ * In PostgreSQL, DISTINCT ON (col) requires the col to appear in SELECT.
125
113
  *
126
- * @param callback - Callback function
127
- * @returns Unsubscribe function
114
+ * @example
115
+ * q.distinctValues("category") // SELECT category … DISTINCT ON (category)
116
+ * q.distinctValues(["category", "status"]) // both fields in DISTINCT ON and SELECT
128
117
  */
118
+ distinctValues(fields) {
119
+ // Record the base DISTINCT flag op
120
+ super.distinctValues(fields);
121
+ // Also add a select for the field(s) so they appear in the SELECT clause
122
+ if (fields) {
123
+ const fieldArr = Array.isArray(fields) ? fields : [fields];
124
+ this.addOperation("select", { fields: fieldArr });
125
+ }
126
+ return this;
127
+ }
128
+ /**
129
+ * Nearest-neighbour vector similarity search via pgvector cosine distance.
130
+ *
131
+ * Adds two operations atomically:
132
+ * 1. `selectRaw` → `1 - (column <=> $n::vector) AS <alias>`
133
+ * Makes the similarity score available on every returned row.
134
+ * 2. `orderByRaw` → `column <=> $n::vector`
135
+ * Tells the PostgreSQL query planner to use the IVFFlat/HNSW vector index.
136
+ * Using the alias in ORDER BY would bypass the index — the raw expression is required.
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * const results = await Vector.query()
141
+ * .where({ organization_id: "org-123", content_type: "summary" })
142
+ * .nearestTo("embedding", queryEmbedding)
143
+ * .limit(5)
144
+ * .get<VectorRow & { score: number }>();
145
+ * ```
146
+ */
147
+ nearestTo(column, embedding, alias = "score") {
148
+ // pgvector expects the literal format: [n,n,n,...]
149
+ const literal = `[${embedding.join(",")}]`;
150
+ const quotedCol = this.driver.dialect.quoteIdentifier(column);
151
+ const quotedTable = this.driver.dialect.quoteIdentifier(this.table);
152
+ // 0 — Preserve all table columns.
153
+ // Adding a selectRaw suppresses the parser's "SELECT *" fallback,
154
+ // so we must explicitly include table.* before the score expression.
155
+ this.addOperation("selectRaw", {
156
+ expression: `${quotedTable}.*`,
157
+ bindings: [],
158
+ });
159
+ // 1 — Add similarity score to SELECT
160
+ this.addOperation("selectRaw", {
161
+ expression: `1 - (${quotedCol} <=> ?::vector) AS ${alias}`,
162
+ bindings: [literal],
163
+ });
164
+ // 2 — ORDER BY the raw expression so the vector index is used
165
+ this.addOperation("orderByRaw", {
166
+ expression: `${quotedCol} <=> ?::vector`,
167
+ bindings: [literal],
168
+ });
169
+ return this;
170
+ }
171
+ /** Set a hydration callback that transforms each result row. */
172
+ hydrate(callback) {
173
+ this.hydrateCallback = callback;
174
+ return this;
175
+ }
176
+ /** Register a callback invoked before query execution. */
129
177
  onFetching(callback) {
130
178
  this.fetchingCallback = callback;
131
179
  return () => {
132
180
  this.fetchingCallback = undefined;
133
181
  };
134
182
  }
135
- /**
136
- * Register callback invoked after fetch but before hydration.
137
- *
138
- * @param callback - Callback function
139
- * @returns Unsubscribe function
140
- */
183
+ /** Register a callback invoked after fetch but before hydration. */
141
184
  onHydrating(callback) {
142
185
  this.hydratingCallback = callback;
143
186
  return () => {
144
187
  this.hydratingCallback = undefined;
145
188
  };
146
189
  }
147
- /**
148
- * Register callback invoked after fetch and hydration.
149
- *
150
- * @param callback - Callback function
151
- * @returns Unsubscribe function
152
- */
190
+ /** Register a callback invoked after fetch and hydration. */
153
191
  onFetched(callback) {
154
192
  this.fetchedCallback = callback;
155
193
  return () => {
@@ -159,1300 +197,407 @@ class PostgresQueryBuilder {
159
197
  // ============================================================================
160
198
  // SCOPES
161
199
  // ============================================================================
162
- /**
163
- * Disable one or more global scopes for this query.
164
- *
165
- * @param scopeNames - Scope names to disable
166
- * @returns This builder for chaining
167
- */
168
- withoutGlobalScope(...scopeNames) {
169
- scopeNames.forEach((name) => this.disabledGlobalScopes.add(name));
170
- return this;
171
- }
172
- /**
173
- * Disable all global scopes for this query.
174
- *
175
- * @returns This builder for chaining
176
- */
177
- withoutGlobalScopes() {
178
- if (this.pendingGlobalScopes) {
179
- this.pendingGlobalScopes.forEach((_, name) => {
180
- this.disabledGlobalScopes.add(name);
181
- });
182
- }
183
- return this;
184
- }
185
- /**
186
- * Apply a local scope to this query.
187
- *
188
- * @param scopeName - Name of the local scope
189
- * @returns This builder for chaining
190
- */
191
- scope(scopeName) {
192
- if (!this.availableLocalScopes) {
193
- throw new Error("No local scopes available");
194
- }
195
- const scopeCallback = this.availableLocalScopes.get(scopeName);
196
- if (!scopeCallback) {
197
- throw new Error(`Local scope "${scopeName}" not found`);
198
- }
199
- scopeCallback(this);
200
- return this;
201
- }
202
- /**
203
- * Apply pending global scopes before query execution.
204
- */
200
+ /** Apply pending global scopes to the operations list. */
205
201
  applyPendingScopes() {
206
- if (!this.pendingGlobalScopes || this.scopesApplied) {
202
+ if (!this.pendingGlobalScopes || this.scopesApplied)
207
203
  return;
208
- }
209
204
  const beforeOps = [];
210
205
  const afterOps = [];
211
206
  for (const [name, { callback, timing }] of this.pendingGlobalScopes) {
212
- if (this.disabledGlobalScopes.has(name)) {
207
+ if (this.disabledGlobalScopes.has(name))
213
208
  continue;
214
- }
215
- const tempBuilder = new PostgresQueryBuilder(this.table, this.dataSource);
216
- callback(tempBuilder);
209
+ const temp = new PostgresQueryBuilder(this.table, this.dataSource);
210
+ callback(temp);
217
211
  if (timing === "before") {
218
- beforeOps.push(...tempBuilder.operations);
212
+ beforeOps.push(...temp.operations);
219
213
  }
220
214
  else {
221
- afterOps.push(...tempBuilder.operations);
215
+ afterOps.push(...temp.operations);
222
216
  }
223
217
  }
224
218
  this.operations = [...beforeOps, ...this.operations, ...afterOps];
225
219
  this.scopesApplied = true;
226
220
  }
227
- where(...args) {
228
- if (args.length === 1) {
229
- if (typeof args[0] === "function") {
230
- // Callback for nested conditions
231
- const tempBuilder = new PostgresQueryBuilder(this.table, this.dataSource);
232
- args[0](tempBuilder);
233
- // Wrap nested operations
234
- this.addOperation("where", { nested: tempBuilder.operations });
235
- }
236
- else if (typeof args[0] === "object") {
237
- // Object conditions
238
- for (const [key, value] of Object.entries(args[0])) {
239
- this.addOperation("where", { field: key, operator: "=", value });
240
- }
241
- }
242
- }
243
- else if (args.length === 2) {
244
- this.addOperation("where", { field: args[0], operator: "=", value: args[1] });
221
+ // ============================================================================
222
+ // WHERE — POSTGRES-SPECIFIC (driver.dialect required)
223
+ // ============================================================================
224
+ /** Array field contains a value (or object with key). */
225
+ whereArrayContains(field, value, key) {
226
+ const quotedField = this.driver.dialect.quoteIdentifier(field);
227
+ if (key) {
228
+ this.addOperation("whereRaw", {
229
+ expression: `${quotedField} @> ?::jsonb`,
230
+ bindings: [JSON.stringify([{ [key]: value }])],
231
+ });
245
232
  }
246
- else if (args.length === 3) {
247
- this.addOperation("where", { field: args[0], operator: args[1], value: args[2] });
233
+ else {
234
+ this.addOperation("whereRaw", {
235
+ expression: `? = ANY(${quotedField})`,
236
+ bindings: [value],
237
+ });
248
238
  }
249
239
  return this;
250
240
  }
251
- orWhere(...args) {
252
- if (args.length === 2) {
253
- this.addOperation("orWhere", { field: args[0], operator: "=", value: args[1] });
241
+ /** Array field does NOT contain a value (or object with key). */
242
+ whereArrayNotContains(field, value, key) {
243
+ const quotedField = this.driver.dialect.quoteIdentifier(field);
244
+ if (key) {
245
+ this.addOperation("whereRaw", {
246
+ expression: `NOT (${quotedField} @> ?::jsonb)`,
247
+ bindings: [JSON.stringify([{ [key]: value }])],
248
+ });
254
249
  }
255
- else if (args.length === 3) {
256
- this.addOperation("orWhere", { field: args[0], operator: args[1], value: args[2] });
250
+ else {
251
+ this.addOperation("whereRaw", {
252
+ expression: `NOT (? = ANY(${quotedField}))`,
253
+ bindings: [value],
254
+ });
257
255
  }
258
256
  return this;
259
257
  }
260
- /**
261
- * Add a raw WHERE clause.
262
- */
263
- whereRaw(expression, bindings) {
264
- this.addOperation("whereRaw", { expression, bindings });
265
- return this;
266
- }
267
- /**
268
- * Add a raw OR WHERE clause.
269
- */
270
- orWhereRaw(expression, bindings) {
271
- this.addOperation("orWhereRaw", { expression, bindings });
272
- return this;
273
- }
274
- // ============================================================================
275
- // WHERE CLAUSES - COLUMN COMPARISONS
276
- // ============================================================================
277
- /**
278
- * Compare two columns.
279
- */
280
- whereColumn(first, operator, second) {
281
- this.addOperation("whereColumn", { first, operator, second });
282
- return this;
283
- }
284
- /**
285
- * Compare two columns with OR.
286
- */
287
- orWhereColumn(first, operator, second) {
288
- this.addOperation("orWhereColumn", { first, operator, second });
289
- return this;
290
- }
291
- /**
292
- * Compare multiple column pairs.
293
- */
294
- whereColumns(comparisons) {
295
- for (const [left, operator, right] of comparisons) {
296
- this.whereColumn(left, operator, right);
258
+ /** Array field contains value OR is empty. */
259
+ whereArrayHasOrEmpty(field, value, key) {
260
+ const quotedField = this.driver.dialect.quoteIdentifier(field);
261
+ if (key) {
262
+ this.addOperation("whereRaw", {
263
+ expression: `(${quotedField} @> ?::jsonb OR ${quotedField} = '[]'::jsonb OR ${quotedField} IS NULL)`,
264
+ bindings: [JSON.stringify([{ [key]: value }])],
265
+ });
266
+ }
267
+ else {
268
+ this.addOperation("whereRaw", {
269
+ expression: `(? = ANY(${quotedField}) OR array_length(${quotedField}, 1) IS NULL)`,
270
+ bindings: [value],
271
+ });
297
272
  }
298
273
  return this;
299
274
  }
300
- /**
301
- * Check if field is between two columns.
302
- */
303
- whereBetweenColumns(field, lowerColumn, upperColumn) {
304
- this.addOperation("whereBetween", { field, lowerColumn, upperColumn, useColumns: true });
305
- return this;
306
- }
307
- // ============================================================================
308
- // WHERE CLAUSES - DATE OPERATIONS
309
- // ============================================================================
310
- /**
311
- * Filter by date (ignoring time).
312
- */
313
- whereDate(field, value) {
314
- this.addOperation("whereDate", { field, value });
315
- return this;
316
- }
317
- /**
318
- * Alias for whereDate.
319
- */
320
- whereDateEquals(field, value) {
321
- return this.whereDate(field, value);
322
- }
323
- /**
324
- * Filter for dates before a value.
325
- */
326
- whereDateBefore(field, value) {
327
- this.addOperation("whereDateBefore", { field, value });
328
- return this;
329
- }
330
- /**
331
- * Filter for dates after a value.
332
- */
333
- whereDateAfter(field, value) {
334
- this.addOperation("whereDateAfter", { field, value });
335
- return this;
336
- }
337
- /**
338
- * Filter by time.
339
- */
340
- whereTime(field, value) {
341
- this.addOperation("where", { field, operator: "=", value, timeOnly: true });
342
- return this;
343
- }
344
- /**
345
- * Filter by day of month.
346
- */
347
- whereDay(field, value) {
348
- this.addOperation("whereRaw", {
349
- expression: `EXTRACT(DAY FROM ${field}) = ?`,
350
- bindings: [value],
351
- });
352
- return this;
353
- }
354
- /**
355
- * Filter by month.
356
- */
357
- whereMonth(field, value) {
358
- this.addOperation("whereRaw", {
359
- expression: `EXTRACT(MONTH FROM ${field}) = ?`,
360
- bindings: [value],
361
- });
362
- return this;
363
- }
364
- /**
365
- * Filter by year.
366
- */
367
- whereYear(field, value) {
368
- this.addOperation("whereRaw", {
369
- expression: `EXTRACT(YEAR FROM ${field}) = ?`,
370
- bindings: [value],
371
- });
372
- return this;
373
- }
374
- // ============================================================================
375
- // WHERE CLAUSES - JSON OPERATIONS
376
- // ============================================================================
377
- /**
378
- * Check if JSON contains value.
379
- */
380
- whereJsonContains(path, value) {
381
- this.addOperation("whereJsonContains", { path, value });
382
- return this;
383
- }
384
- /**
385
- * Check if JSON doesn't contain value.
386
- */
387
- whereJsonDoesntContain(path, value) {
388
- this.addOperation("whereJsonDoesntContain", { path, value });
389
- return this;
390
- }
391
- /**
392
- * Check if JSON contains key.
393
- */
394
- whereJsonContainsKey(path) {
395
- this.addOperation("whereRaw", {
396
- expression: `${path} IS NOT NULL`,
397
- bindings: [],
398
- });
399
- return this;
400
- }
401
- /**
402
- * Check JSON array/string length.
403
- */
404
- whereJsonLength(path, operator, value) {
405
- this.addOperation("whereRaw", {
406
- expression: `jsonb_array_length(${path}) ${operator} ?`,
407
- bindings: [value],
408
- });
409
- return this;
410
- }
411
- /**
412
- * Check if JSON is array.
413
- */
414
- whereJsonIsArray(path) {
415
- this.addOperation("whereRaw", {
416
- expression: `jsonb_typeof(${path}) = 'array'`,
417
- bindings: [],
418
- });
419
- return this;
420
- }
421
- /**
422
- * Check if JSON is object.
423
- */
424
- whereJsonIsObject(path) {
425
- this.addOperation("whereRaw", {
426
- expression: `jsonb_typeof(${path}) = 'object'`,
427
- bindings: [],
428
- });
429
- return this;
430
- }
431
- /**
432
- * Check array length.
433
- */
434
- whereArrayLength(field, operator, value) {
435
- this.addOperation("whereRaw", {
436
- expression: `array_length(${field}, 1) ${operator} ?`,
437
- bindings: [value],
438
- });
275
+ /** Array field does NOT contain value OR is empty. */
276
+ whereArrayNotHaveOrEmpty(field, value, key) {
277
+ const quotedField = this.driver.dialect.quoteIdentifier(field);
278
+ if (key) {
279
+ this.addOperation("whereRaw", {
280
+ expression: `(NOT (${quotedField} @> ?::jsonb) OR ${quotedField} = '[]'::jsonb OR ${quotedField} IS NULL)`,
281
+ bindings: [JSON.stringify([{ [key]: value }])],
282
+ });
283
+ }
284
+ else {
285
+ this.addOperation("whereRaw", {
286
+ expression: `(NOT (? = ANY(${quotedField})) OR array_length(${quotedField}, 1) IS NULL)`,
287
+ bindings: [value],
288
+ });
289
+ }
439
290
  return this;
440
291
  }
441
292
  // ============================================================================
442
- // WHERE CLAUSES - CONVENIENCE METHODS
293
+ // joinWith RESOLVE RELATION DEFINITIONS
443
294
  // ============================================================================
444
295
  /**
445
- * Filter by ID.
446
- */
447
- whereId(value) {
448
- return this.where("id", value);
449
- }
450
- /**
451
- * Filter by multiple IDs.
452
- */
453
- whereIds(values) {
454
- return this.whereIn("id", values);
455
- }
456
- /**
457
- * Filter by UUID.
458
- */
459
- whereUuid(value) {
460
- return this.where("uuid", value);
461
- }
462
- /**
463
- * Filter by ULID.
464
- */
465
- whereUlid(value) {
466
- return this.where("ulid", value);
467
- }
468
- /**
469
- * Full-text search.
470
- */
471
- whereFullText(fields, query) {
472
- const fieldList = Array.isArray(fields) ? fields : [fields];
473
- this.addOperation("whereFullText", { fields: fieldList, query });
474
- return this;
475
- }
476
- /**
477
- * Full-text search with OR.
478
- */
479
- orWhereFullText(fields, query) {
480
- // TODO: Handle OR full-text
481
- return this.whereFullText(fields, query);
482
- }
483
- /**
484
- * Alias for whereFullText.
485
- */
486
- whereSearch(field, query) {
487
- return this.whereFullText(field, query);
488
- }
489
- /**
490
- * Negate conditions.
491
- */
492
- whereNot(callback) {
493
- // TODO: Implement NOT wrapper
494
- return this;
495
- }
496
- /**
497
- * Negate conditions with OR.
498
- */
499
- orWhereNot(callback) {
500
- // TODO: Implement OR NOT wrapper
501
- return this;
502
- }
503
- // ============================================================================
504
- // WHERE CLAUSES - COMPARISON OPERATORS
505
- // ============================================================================
506
- /**
507
- * Filter by value in array.
508
- */
509
- whereIn(field, values) {
510
- this.addOperation("whereIn", { field, values });
511
- return this;
512
- }
513
- /**
514
- * Filter by value not in array.
515
- */
516
- whereNotIn(field, values) {
517
- this.addOperation("whereNotIn", { field, values });
518
- return this;
519
- }
520
- /**
521
- * Filter by NULL value.
522
- */
523
- whereNull(field) {
524
- this.addOperation("whereNull", { field });
525
- return this;
526
- }
527
- /**
528
- * Filter by NOT NULL value.
529
- */
530
- whereNotNull(field) {
531
- this.addOperation("whereNotNull", { field });
532
- return this;
533
- }
534
- /**
535
- * Filter by range (inclusive).
536
- */
537
- whereBetween(field, range) {
538
- this.addOperation("whereBetween", { field, range });
539
- return this;
540
- }
541
- /**
542
- * Filter by not in range.
543
- */
544
- whereNotBetween(field, range) {
545
- this.addOperation("whereNotBetween", { field, range });
546
- return this;
547
- }
548
- // ============================================================================
549
- // WHERE CLAUSES - PATTERN MATCHING
550
- // ============================================================================
551
- /**
552
- * Filter by LIKE pattern (case-insensitive).
553
- */
554
- whereLike(field, pattern) {
555
- const patternStr = pattern instanceof RegExp ? pattern.source : pattern;
556
- this.addOperation("whereLike", { field, pattern: patternStr });
557
- return this;
558
- }
559
- /**
560
- * Filter by NOT LIKE pattern.
561
- */
562
- whereNotLike(field, pattern) {
563
- const patternStr = pattern instanceof RegExp ? pattern.source : pattern;
564
- this.addOperation("whereNotLike", { field, pattern: patternStr });
565
- return this;
566
- }
567
- /**
568
- * Filter by prefix.
569
- */
570
- whereStartsWith(field, value) {
571
- return this.whereLike(field, `${value}%`);
572
- }
573
- /**
574
- * Filter by not starting with prefix.
575
- */
576
- whereNotStartsWith(field, value) {
577
- return this.whereNotLike(field, `${value}%`);
578
- }
579
- /**
580
- * Filter by suffix.
581
- */
582
- whereEndsWith(field, value) {
583
- return this.whereLike(field, `%${value}`);
584
- }
585
- /**
586
- * Filter by not ending with suffix.
587
- */
588
- whereNotEndsWith(field, value) {
589
- return this.whereNotLike(field, `%${value}`);
590
- }
591
- /**
592
- * Filter by date range.
593
- */
594
- whereDateBetween(field, range) {
595
- this.addOperation("whereDateBetween", { field, range });
596
- return this;
597
- }
598
- /**
599
- * Filter by not in date range.
600
- */
601
- whereDateNotBetween(field, range) {
602
- // Use NOT BETWEEN
603
- this.addOperation("whereNotBetween", { field, range });
604
- return this;
605
- }
606
- whereExists(param) {
607
- if (typeof param === "function") {
608
- // Subquery exists
609
- const tempBuilder = new PostgresQueryBuilder(this.table, this.dataSource);
610
- param(tempBuilder);
611
- this.addOperation("whereExists", { subquery: tempBuilder.operations });
612
- }
613
- else {
614
- this.addOperation("whereNotNull", { field: param });
615
- }
616
- return this;
617
- }
618
- whereNotExists(param) {
619
- if (typeof param === "function") {
620
- const tempBuilder = new PostgresQueryBuilder(this.table, this.dataSource);
621
- param(tempBuilder);
622
- this.addOperation("whereNotExists", { subquery: tempBuilder.operations });
623
- }
624
- else {
625
- this.addOperation("whereNull", { field: param });
626
- }
627
- return this;
628
- }
629
- whereSize(field, ...args) {
630
- const operator = args.length === 2 ? args[0] : "=";
631
- const size = args.length === 2 ? args[1] : args[0];
632
- return this.whereArrayLength(field, operator, size);
633
- }
634
- /**
635
- * Perform a full-text search.
636
- */
637
- textSearch(query, filters) {
638
- // Apply filters if provided
639
- if (filters) {
640
- for (const [key, value] of Object.entries(filters)) {
641
- this.where(key, value);
642
- }
643
- }
644
- // Full-text search would need to know which columns to search
645
- // For now, this is a placeholder - users should use whereFullText directly
646
- return this;
647
- }
648
- /**
649
- * Constrain an array field to contain the given value.
650
- */
651
- whereArrayContains(field, value, key) {
652
- if (key) {
653
- // Array of objects - use JSON containment check
654
- this.addOperation("whereRaw", {
655
- expression: `${this.driver.dialect.quoteIdentifier(field)} @> ?::jsonb`,
656
- bindings: [JSON.stringify([{ [key]: value }])],
657
- });
658
- }
659
- else {
660
- // Simple array - check if value is in array
661
- this.addOperation("whereRaw", {
662
- expression: `? = ANY(${this.driver.dialect.quoteIdentifier(field)})`,
663
- bindings: [value],
664
- });
665
- }
666
- return this;
667
- }
668
- /**
669
- * Constrain an array field to not contain the given value.
670
- */
671
- whereArrayNotContains(field, value, key) {
672
- if (key) {
673
- this.addOperation("whereRaw", {
674
- expression: `NOT (${this.driver.dialect.quoteIdentifier(field)} @> ?::jsonb)`,
675
- bindings: [JSON.stringify([{ [key]: value }])],
676
- });
677
- }
678
- else {
679
- this.addOperation("whereRaw", {
680
- expression: `NOT (? = ANY(${this.driver.dialect.quoteIdentifier(field)}))`,
681
- bindings: [value],
682
- });
683
- }
684
- return this;
685
- }
686
- /**
687
- * Constrain an array field to contain the value OR be empty.
688
- */
689
- whereArrayHasOrEmpty(field, value, key) {
690
- const quotedField = this.driver.dialect.quoteIdentifier(field);
691
- if (key) {
692
- this.addOperation("whereRaw", {
693
- expression: `(${quotedField} @> ?::jsonb OR ${quotedField} = '[]'::jsonb OR ${quotedField} IS NULL)`,
694
- bindings: [JSON.stringify([{ [key]: value }])],
695
- });
696
- }
697
- else {
698
- this.addOperation("whereRaw", {
699
- expression: `(? = ANY(${quotedField}) OR array_length(${quotedField}, 1) IS NULL)`,
700
- bindings: [value],
701
- });
702
- }
703
- return this;
704
- }
705
- /**
706
- * Constrain an array field to not contain the value OR be empty.
707
- */
708
- whereArrayNotHaveOrEmpty(field, value, key) {
709
- const quotedField = this.driver.dialect.quoteIdentifier(field);
710
- if (key) {
711
- this.addOperation("whereRaw", {
712
- expression: `(NOT (${quotedField} @> ?::jsonb) OR ${quotedField} = '[]'::jsonb OR ${quotedField} IS NULL)`,
713
- bindings: [JSON.stringify([{ [key]: value }])],
714
- });
715
- }
716
- else {
717
- this.addOperation("whereRaw", {
718
- expression: `(NOT (? = ANY(${quotedField})) OR array_length(${quotedField}, 1) IS NULL)`,
719
- bindings: [value],
720
- });
721
- }
722
- return this;
723
- }
724
- join(...args) {
725
- if (args.length === 3) {
726
- this.addOperation("join", {
727
- table: args[0],
728
- localField: args[1],
729
- foreignField: args[2],
730
- });
731
- }
732
- else {
733
- this.addOperation("join", args[0]);
734
- }
735
- return this;
736
- }
737
- leftJoin(...args) {
738
- if (args.length === 3) {
739
- this.addOperation("leftJoin", {
740
- table: args[0],
741
- localField: args[1],
742
- foreignField: args[2],
743
- });
744
- }
745
- else {
746
- this.addOperation("leftJoin", args[0]);
747
- }
748
- return this;
749
- }
750
- rightJoin(...args) {
751
- if (args.length === 3) {
752
- this.addOperation("rightJoin", {
753
- table: args[0],
754
- localField: args[1],
755
- foreignField: args[2],
756
- });
757
- }
758
- else {
759
- this.addOperation("rightJoin", args[0]);
760
- }
761
- return this;
762
- }
763
- innerJoin(...args) {
764
- if (args.length === 3) {
765
- this.addOperation("innerJoin", {
766
- table: args[0],
767
- localField: args[1],
768
- foreignField: args[2],
769
- });
770
- }
771
- else {
772
- this.addOperation("innerJoin", args[0]);
773
- }
774
- return this;
775
- }
776
- fullJoin(...args) {
777
- if (args.length === 3) {
778
- this.addOperation("fullJoin", {
779
- table: args[0],
780
- localField: args[1],
781
- foreignField: args[2],
782
- });
783
- }
784
- else {
785
- this.addOperation("fullJoin", args[0]);
786
- }
787
- return this;
788
- }
789
- /**
790
- * Add a CROSS JOIN clause.
791
- */
792
- crossJoin(table) {
793
- this.addOperation("crossJoin", { table });
794
- return this;
795
- }
796
- /**
797
- * Add a raw JOIN clause.
798
- */
799
- joinRaw(expression, bindings) {
800
- this.addOperation("joinRaw", { expression, bindings });
801
- return this;
802
- }
803
- select(...args) {
804
- // Handle single array argument
805
- if (args.length === 1 && Array.isArray(args[0])) {
806
- this.addOperation("select", { fields: args[0] });
807
- }
808
- // Handle Record<string, 0 | 1 | boolean> (projection map)
809
- else if (args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0])) {
810
- this.addOperation("select", { fields: args[0] });
811
- }
812
- // Handle rest params (...fields)
813
- else {
814
- const flatFields = args.flat();
815
- this.addOperation("select", { fields: flatFields });
816
- }
817
- return this;
818
- }
819
- /**
820
- * Select a field with alias.
821
- */
822
- selectAs(field, alias) {
823
- // Use select operation with alias format (Record<field, alias>)
824
- this.addOperation("select", { fields: { [field]: alias } });
825
- return this;
826
- }
827
- /**
828
- * Select raw expression.
829
- */
830
- selectRaw(expression, bindings) {
831
- this.addOperation("selectRaw", { expression, bindings });
832
- return this;
833
- }
834
- /**
835
- * Select multiple raw expressions.
836
- */
837
- selectRawMany(definitions) {
838
- for (const def of definitions) {
839
- this.selectRaw({ [def.alias]: def.expression }, def.bindings);
840
- }
841
- return this;
842
- }
843
- /**
844
- * Select subquery.
845
- */
846
- selectSub(expression, alias) {
847
- this.addOperation("selectRaw", { expression: { [alias]: expression } });
848
- return this;
849
- }
850
- /**
851
- * Add subquery to existing selection.
852
- */
853
- addSelectSub(expression, alias) {
854
- return this.selectSub(expression, alias);
855
- }
856
- /**
857
- * Select aggregate value.
858
- */
859
- selectAggregate(field, aggregate, alias) {
860
- const expr = `${aggregate.toUpperCase()}(${field})`;
861
- return this.selectRaw({ [alias]: expr });
862
- }
863
- /**
864
- * Select existence check.
865
- */
866
- selectExists(field, alias) {
867
- return this.selectRaw({ [alias]: `${field} IS NOT NULL` });
868
- }
869
- /**
870
- * Select count.
871
- */
872
- selectCount(field, alias) {
873
- return this.selectAggregate(field, "count", alias);
874
- }
875
- /**
876
- * Select CASE expression.
877
- */
878
- selectCase(cases, otherwise, alias) {
879
- const caseExpr = cases.map((c) => `WHEN ${c.when} THEN ${c.then}`).join(" ");
880
- return this.selectRaw({ [alias]: `CASE ${caseExpr} ELSE ${otherwise} END` });
881
- }
882
- /**
883
- * Select conditional (IF/ELSE).
884
- */
885
- selectWhen(condition, thenValue, elseValue, alias) {
886
- return this.selectRaw({
887
- [alias]: `CASE WHEN ${condition} THEN ${thenValue} ELSE ${elseValue} END`,
888
- });
889
- }
890
- /**
891
- * Direct projection manipulation.
892
- */
893
- selectDriverProjection(callback) {
894
- // PostgreSQL doesn't have direct projection manipulation like MongoDB
895
- return this;
896
- }
897
- /**
898
- * Select JSON path.
899
- */
900
- selectJson(path, alias) {
901
- const parts = path.split("->");
902
- const column = parts[0];
903
- const jsonPath = parts.slice(1).join("->");
904
- const expr = jsonPath ? `${column}->>'${jsonPath}'` : column;
905
- return alias ? this.selectAs(expr, alias) : this.selectRaw(expr);
906
- }
907
- /**
908
- * Select JSON path with raw expression.
909
- */
910
- selectJsonRaw(path, expression, alias) {
911
- return this.selectRaw({ [alias]: expression });
912
- }
913
- /**
914
- * Exclude JSON path.
915
- */
916
- deselectJson(path) {
917
- return this.deselect([path]);
918
- }
919
- /**
920
- * Concatenate fields.
921
- */
922
- selectConcat(fields, alias) {
923
- const expr = fields.join(" || ");
924
- return this.selectRaw({ [alias]: expr });
925
- }
926
- /**
927
- * Coalesce values.
928
- */
929
- selectCoalesce(fields, alias) {
930
- const expr = `COALESCE(${fields.join(", ")})`;
931
- return this.selectRaw({ [alias]: expr });
932
- }
933
- /**
934
- * Window function.
935
- */
936
- selectWindow(spec) {
937
- this.addOperation("selectRaw", { expression: spec });
938
- return this;
939
- }
940
- /**
941
- * Exclude columns from projection.
942
- */
943
- deselect(fields) {
944
- this.addOperation("deselect", { fields });
945
- return this;
946
- }
947
- /**
948
- * Clear selection.
949
- */
950
- clearSelect() {
951
- this.operations = this.operations.filter((op) => !op.type.startsWith("select") && op.type !== "deselect");
952
- return this;
953
- }
954
- /**
955
- * Select all columns.
956
- */
957
- selectAll() {
958
- return this.clearSelect();
959
- }
960
- /**
961
- * Restore default projection.
962
- */
963
- selectDefault() {
964
- return this.clearSelect();
965
- }
966
- /**
967
- * Select distinct values.
968
- */
969
- distinctValues(fields) {
970
- this.addOperation("distinct", {});
971
- if (fields) {
972
- this.select(Array.isArray(fields) ? fields : [fields]);
973
- }
974
- return this;
975
- }
976
- /**
977
- * Add additional select fields.
978
- */
979
- addSelect(fields) {
980
- this.addOperation("select", { fields, add: true });
981
- return this;
982
- }
983
- orderBy(...args) {
984
- if (typeof args[0] === "string") {
985
- this.addOperation("orderBy", { field: args[0], direction: args[1] ?? "asc" });
986
- }
987
- else {
988
- for (const [field, direction] of Object.entries(args[0])) {
989
- this.addOperation("orderBy", { field, direction });
296
+ * Load relations via SQL JOINs (single query) with optional per-relation constraints.
297
+ *
298
+ * Supports:
299
+ * - `joinWith("author")` / `joinWith(["author", "category"])`
300
+ * - `joinWith({ actions: q => q.where("status", "pending").limit(5) })`
301
+ * - `joinWith({ organizationAiModel: "id,name", actions: q => q.orderBy("sort_order") })`
302
+ *
303
+ * @example
304
+ * ChatMessage.joinWith({
305
+ * actions: q => q.where("status", "pending").orderBy("sort_order", "asc").limit(5),
306
+ * organizationAiModel: "id,createdAt",
307
+ * })
308
+ */
309
+ joinWith(...args) {
310
+ const entries = [];
311
+ for (const arg of args) {
312
+ if (typeof arg === "string") {
313
+ entries.push({ path: arg });
990
314
  }
991
- }
992
- return this;
993
- }
994
- /**
995
- * Order descending.
996
- */
997
- orderByDesc(field) {
998
- return this.orderBy(field, "desc");
999
- }
1000
- /**
1001
- * Order with raw expression.
1002
- */
1003
- orderByRaw(expression, bindings) {
1004
- this.addOperation("orderByRaw", { expression, bindings });
1005
- return this;
1006
- }
1007
- /**
1008
- * Order randomly.
1009
- */
1010
- orderByRandom(limit) {
1011
- this.addOperation("orderByRaw", { expression: "RANDOM()" });
1012
- return this.limit(limit);
1013
- }
1014
- /**
1015
- * Get latest records.
1016
- */
1017
- async latest(column = "createdAt") {
1018
- return this.orderBy(column, "desc").get();
1019
- }
1020
- /**
1021
- * Get oldest records.
1022
- */
1023
- oldest(column = "createdAt") {
1024
- return this.orderBy(column, "asc");
1025
- }
1026
- // ============================================================================
1027
- // LIMITING / PAGINATION
1028
- // ============================================================================
1029
- /**
1030
- * Limit results.
1031
- */
1032
- limit(value) {
1033
- this.addOperation("limit", { value });
1034
- return this;
1035
- }
1036
- /**
1037
- * Skip results.
1038
- */
1039
- skip(value) {
1040
- this.addOperation("offset", { value });
1041
- return this;
1042
- }
1043
- /**
1044
- * Offset results.
1045
- */
1046
- offset(value) {
1047
- return this.skip(value);
1048
- }
1049
- /**
1050
- * Take first N results.
1051
- */
1052
- take(value) {
1053
- return this.limit(value);
1054
- }
1055
- /**
1056
- * Apply cursor pagination hints.
1057
- */
1058
- cursor(after, before) {
1059
- // Store cursor hints for cursorPaginate
1060
- return this;
1061
- }
1062
- // ============================================================================
1063
- // GROUPING / AGGREGATION
1064
- // ============================================================================
1065
- /**
1066
- * Group results.
1067
- */
1068
- groupBy(input) {
1069
- const fields = Array.isArray(input) ? input : [input];
1070
- this.addOperation("groupBy", { fields });
1071
- return this;
1072
- }
1073
- /**
1074
- * Raw GROUP BY.
1075
- */
1076
- groupByRaw(expression, bindings) {
1077
- this.addOperation("groupBy", { expression, bindings });
1078
- return this;
1079
- }
1080
- having(...args) {
1081
- if (args.length === 1) {
1082
- const input = args[0];
1083
- if (Array.isArray(input)) {
1084
- if (input.length === 2) {
1085
- this.addOperation("having", { field: input[0], operator: "=", value: input[1] });
1086
- }
1087
- else {
1088
- this.addOperation("having", { field: input[0], operator: input[1], value: input[2] });
315
+ else if (Array.isArray(arg)) {
316
+ for (const rel of arg) {
317
+ entries.push({ path: rel });
1089
318
  }
1090
319
  }
1091
- else {
1092
- for (const [key, value] of Object.entries(input)) {
1093
- this.addOperation("having", { field: key, operator: "=", value });
320
+ else if (typeof arg === "object" && arg !== null) {
321
+ for (const [rel, val] of Object.entries(arg)) {
322
+ entries.push({ path: rel, constraint: val });
1094
323
  }
1095
324
  }
1096
325
  }
1097
- else if (args.length === 2) {
1098
- this.addOperation("having", { field: args[0], operator: "=", value: args[1] });
1099
- }
1100
- else if (args.length === 3) {
1101
- this.addOperation("having", { field: args[0], operator: args[1], value: args[2] });
1102
- }
1103
- return this;
1104
- }
1105
- /**
1106
- * Raw HAVING clause.
1107
- */
1108
- havingRaw(expression, bindings) {
1109
- this.addOperation("havingRaw", { expression, bindings });
1110
- return this;
1111
- }
1112
- // ============================================================================
1113
- // EXECUTION METHODS
1114
- // ============================================================================
1115
- /**
1116
- * Execute query and get all results.
1117
- */
1118
- async get() {
1119
- this.applyPendingScopes();
1120
- // Apply JOIN operations for joinWith() relations
1121
- this.applyJoinRelations();
1122
- if (this.fetchingCallback) {
1123
- await this.fetchingCallback(this);
1124
- }
1125
- const parser = new PostgresQueryParser({
1126
- table: this.table,
1127
- operations: this.operations,
1128
- });
1129
- const { sql, params } = parser.parse();
1130
- try {
1131
- const result = await this.driver.query(sql, params);
1132
- let records = result.rows;
1133
- // Extract joined relation data before hydration
1134
- const joinedData = this.extractJoinedRelationData(records);
1135
- if (this.hydratingCallback) {
1136
- await this.hydratingCallback(records, {});
1137
- }
1138
- if (this.hydrateCallback) {
1139
- records = records.map((row, index) => this.hydrateCallback(row, index));
1140
- }
1141
- // Attach joined relations to hydrated models
1142
- this.attachJoinedRelations(records, joinedData);
1143
- if (this.fetchedCallback) {
1144
- await this.fetchedCallback(records, {});
1145
- }
1146
- // Cleanup
1147
- this.operations = [];
1148
- return records;
1149
- }
1150
- catch (error) {
1151
- console.log("Error while executing:", sql, params);
1152
- console.log("Query Builder Error:", error);
1153
- throw error;
1154
- }
1155
- }
1156
- /**
1157
- * Apply JOIN operations for joinWith() relations.
1158
- */
1159
- applyJoinRelations() {
1160
- if (this.joinRelations.size === 0 || !this.relationDefinitions)
1161
- return;
1162
- for (const [relationName, config] of this.joinRelations) {
1163
- const relationDef = this.relationDefinitions[relationName];
1164
- if (!relationDef)
1165
- continue;
1166
- // Resolve the related model class
1167
- const RelatedModel = typeof relationDef.model === "string"
1168
- ? getModelFromRegistry(relationDef.model)
1169
- : relationDef.model;
1170
- if (!RelatedModel)
1171
- continue;
1172
- const relatedTable = RelatedModel.table;
1173
- const alias = config.alias;
1174
- // Determine join keys based on relation type
1175
- let localField;
1176
- let foreignField;
1177
- if (relationDef.type === "belongsTo") {
1178
- localField = relationDef.foreignKey || `${relationName}Id`;
1179
- foreignField = relationDef.ownerKey || "id";
1180
- }
1181
- else {
1182
- // hasOne, hasMany
1183
- localField = relationDef.localKey || "id";
1184
- foreignField = relationDef.foreignKey || `${this.table.slice(0, -1)}Id`;
1185
- }
1186
- // Add LEFT JOIN operation
1187
- this.addOperation("leftJoin", {
1188
- table: relatedTable,
1189
- alias,
1190
- localField,
1191
- foreignField,
1192
- });
1193
- // Add SELECT for related columns with prefix
1194
- this.addOperation("selectRelatedColumns", {
1195
- alias,
1196
- relationName,
1197
- table: relatedTable,
1198
- });
1199
- }
1200
- }
1201
- /**
1202
- * Extract joined relation data from raw records.
1203
- * Returns a map of record index to relation data.
1204
- */
1205
- extractJoinedRelationData(records) {
1206
- const result = new Map();
1207
- if (this.joinRelations.size === 0)
1208
- return result;
1209
- records.forEach((record, index) => {
1210
- const relationData = {};
1211
- for (const [relationName, config] of this.joinRelations) {
1212
- const columnName = config.alias; // e.g., "_rel_author"
1213
- // Get the JSON object from the row_to_json column
1214
- const relatedData = record[columnName];
1215
- // Remove from main record so it doesn't appear in model.data
1216
- delete record[columnName];
1217
- // If null or all values are null, set to null
1218
- if (relatedData === null ||
1219
- (typeof relatedData === "object" && Object.values(relatedData).every((v) => v === null))) {
1220
- relationData[relationName] = null;
1221
- }
1222
- else {
1223
- relationData[relationName] = relatedData;
1224
- }
1225
- }
1226
- result.set(index, relationData);
1227
- });
1228
- return result;
1229
- }
1230
- /**
1231
- * Attach joined relations to hydrated models.
1232
- */
1233
- attachJoinedRelations(records, joinedData) {
1234
- if (this.joinRelations.size === 0 || !this.relationDefinitions)
1235
- return;
1236
- records.forEach((model, index) => {
1237
- const relationData = joinedData.get(index);
1238
- if (!relationData)
1239
- return;
1240
- for (const [relationName, data] of Object.entries(relationData)) {
1241
- if (data === null) {
1242
- // No related record
1243
- model[relationName] = null;
1244
- if (model.loadedRelations) {
1245
- model.loadedRelations.set(relationName, null);
326
+ for (const { path, constraint } of entries) {
327
+ // Parse each dot-notation path segment (supports "rel1.rel2" nesting)
328
+ const segments = path.split(".");
329
+ let currentModel = this.modelClass;
330
+ let currentPath = "";
331
+ for (let i = 0; i < segments.length; i++) {
332
+ const rawSeg = segments[i];
333
+ // String shorthand: "relName:col1,col2"
334
+ const colonIdx = rawSeg.indexOf(":");
335
+ const segName = colonIdx === -1 ? rawSeg : rawSeg.slice(0, colonIdx);
336
+ const segColumns = colonIdx === -1
337
+ ? undefined
338
+ : rawSeg
339
+ .slice(colonIdx + 1)
340
+ .split(",")
341
+ .filter(Boolean);
342
+ currentPath = currentPath ? `${currentPath}.${segName}` : segName;
343
+ // If already registered, update if new select columns given; advance model
344
+ if (this.joinRelations.has(currentPath)) {
345
+ const existing = this.joinRelations.get(currentPath);
346
+ if (segColumns)
347
+ existing.select = segColumns;
348
+ // Apply constraint only on the deepest segment
349
+ if (i === segments.length - 1 && constraint !== undefined) {
350
+ existing.constraintOps = this._resolveConstraintOps(constraint);
1246
351
  }
352
+ currentModel =
353
+ typeof existing.model === "string"
354
+ ? getModelFromRegistry(existing.model)
355
+ : existing.model;
1247
356
  continue;
1248
357
  }
1249
- const relationDef = this.relationDefinitions[relationName];
1250
- if (!relationDef)
358
+ if (!this.relationDefinitions)
1251
359
  continue;
1252
- // Resolve and hydrate the related model
1253
- const RelatedModel = typeof relationDef.model === "string"
1254
- ? getModelFromRegistry(relationDef.model)
1255
- : relationDef.model;
1256
- if (RelatedModel) {
1257
- const relatedInstance = new RelatedModel(data);
1258
- relatedInstance.isNew = false;
1259
- model[relationName] = relatedInstance;
1260
- if (model.loadedRelations) {
1261
- model.loadedRelations.set(relationName, relatedInstance);
360
+ const def = (i === 0
361
+ ? this.relationDefinitions
362
+ : currentModel?.relations)?.[segName];
363
+ if (!def) {
364
+ throw new Error(`Relation "${segName}" not found on model ${currentModel?.name ?? "unknown"}`);
365
+ }
366
+ // Resolve select columns: colon shorthand > constraint string > def.select
367
+ let selectColumns = segColumns ?? def.select;
368
+ let constraintOps;
369
+ if (i === segments.length - 1 && constraint !== undefined) {
370
+ if (typeof constraint === "string") {
371
+ selectColumns = constraint.split(",").filter(Boolean);
372
+ }
373
+ else {
374
+ constraintOps = this._resolveConstraintOps(constraint);
1262
375
  }
1263
376
  }
377
+ const alias = currentPath.replace(/\./g, "_");
378
+ this.joinRelations.set(currentPath, {
379
+ alias,
380
+ type: def.type,
381
+ model: def.model,
382
+ localKey: def.localKey,
383
+ foreignKey: def.foreignKey,
384
+ ownerKey: def.ownerKey,
385
+ parentPath: i > 0 ? currentPath.substring(0, currentPath.lastIndexOf(".")) : null,
386
+ relationName: segName,
387
+ parentModel: currentModel,
388
+ select: selectColumns,
389
+ constraintOps,
390
+ });
391
+ currentModel =
392
+ typeof def.model === "string" ? getModelFromRegistry(def.model) : def.model;
393
+ if (!currentModel) {
394
+ throw new Error(`Relation model not found for "${segName}" in "${currentPath}"`);
395
+ }
1264
396
  }
1265
- });
397
+ }
398
+ return this;
1266
399
  }
400
+ /** Run a joinWith constraint callback against a sub-QB and capture its operations. */
401
+ _resolveConstraintOps(constraint) {
402
+ if (typeof constraint === "string")
403
+ return [];
404
+ const sub = new PostgresQueryBuilder("__sub__", this.dataSource);
405
+ constraint(sub);
406
+ return sub.operations;
407
+ }
408
+ // ============================================================================
409
+ // EXECUTION METHODS
410
+ // ============================================================================
1267
411
  /**
1268
- * Get first result.
412
+ * Execute the query and return all matching rows.
1269
413
  */
414
+ async get() {
415
+ this.applyPendingScopes();
416
+ this._processJoinWithOps();
417
+ this.applyJoinRelations();
418
+ if (this.fetchingCallback) {
419
+ await this.fetchingCallback(this);
420
+ }
421
+ const parser = new PostgresQueryParser({
422
+ table: this.table,
423
+ operations: toParserOps(this.operations),
424
+ });
425
+ const { query = "", bindings = [] } = parser.parse();
426
+ try {
427
+ const result = await this.driver.query(query, bindings);
428
+ let records = result.rows;
429
+ const joinedData = this.extractJoinedRelationData(records);
430
+ if (this.hydratingCallback) {
431
+ await this.hydratingCallback(records, {});
432
+ }
433
+ if (this.hydrateCallback) {
434
+ records = records.map((row, index) => this.hydrateCallback(row, index));
435
+ }
436
+ this.attachJoinedRelations(records, joinedData);
437
+ if (this.fetchedCallback) {
438
+ await this.fetchedCallback(records, {});
439
+ }
440
+ this.operations = [];
441
+ return records;
442
+ }
443
+ catch (error) {
444
+ console.log("Error while executing:", query, bindings);
445
+ console.log("Query Builder Error:", error);
446
+ throw error;
447
+ }
448
+ }
449
+ /** Get first result. */
1270
450
  async first() {
1271
451
  const results = await this.limit(1).get();
1272
452
  return results[0] ?? null;
1273
453
  }
1274
- /**
1275
- * Get last result.
1276
- */
454
+ /** Get last result (by id desc). */
1277
455
  async last() {
1278
456
  const results = await this.orderByDesc("id").limit(1).get();
1279
457
  return results[0] ?? null;
1280
458
  }
1281
- /**
1282
- * Get random results.
1283
- */
459
+ /** Get random results. */
1284
460
  async random(limit) {
1285
461
  this.orderByRaw("RANDOM()");
1286
- if (limit) {
462
+ if (limit)
1287
463
  this.limit(limit);
1288
- }
1289
464
  return this.get();
1290
465
  }
1291
- /**
1292
- * Get first or throw.
1293
- */
466
+ /** Get first or throw. */
1294
467
  async firstOrFail() {
1295
468
  const result = await this.first();
1296
- if (!result) {
469
+ if (!result)
1297
470
  throw new Error("No records found");
1298
- }
1299
471
  return result;
1300
472
  }
1301
- /**
1302
- * Get first or call callback.
1303
- */
473
+ /** Get first or call callback. */
1304
474
  async firstOr(callback) {
1305
475
  const result = await this.first();
1306
476
  return result ?? (await callback());
1307
477
  }
1308
- /**
1309
- * Get first or return default.
1310
- */
478
+ /** Get first or return null. */
1311
479
  async firstOrNull() {
1312
480
  return this.first();
1313
481
  }
1314
- /**
1315
- * Get first or create new.
1316
- */
482
+ /** Get first or return default. */
1317
483
  async firstOrNew(defaults) {
1318
484
  const result = await this.first();
1319
485
  return result ?? defaults;
1320
486
  }
1321
- /**
1322
- * Find by ID.
1323
- */
487
+ /** Find by primary key. */
1324
488
  async find(id) {
1325
489
  return this.where("id", id).first();
1326
490
  }
1327
- /**
1328
- * Count results.
1329
- */
491
+ /** Count matching rows. */
1330
492
  async count() {
1331
493
  this.applyPendingScopes();
1332
- // Build count query using selectRaw to avoid quoting COUNT(*) as a column
1333
- const countOps = [
494
+ const countOps = toParserOps([
1334
495
  ...this.operations.filter((op) => op.type.includes("where") || op.type.includes("join")),
1335
496
  { type: "selectRaw", data: { expression: 'COUNT(*) AS "count"' } },
1336
- ];
1337
- const parser = new PostgresQueryParser({
1338
- table: this.table,
1339
- operations: countOps,
1340
- });
1341
- const { sql, params } = parser.parse();
1342
- const result = await this.driver.query(sql, params);
497
+ ]);
498
+ const parser = new PostgresQueryParser({ table: this.table, operations: countOps });
499
+ const { query = "", bindings = [] } = parser.parse();
500
+ const result = await this.driver.query(query, bindings);
1343
501
  return parseInt(result.rows[0]?.count ?? "0", 10);
1344
502
  }
1345
- /**
1346
- * Sum of field values.
1347
- */
503
+ /** SUM a numeric field. */
1348
504
  async sum(field) {
1349
505
  this.applyPendingScopes();
1350
506
  const result = await this.selectRaw(`SUM(${field}) as sum`).first();
1351
507
  return parseFloat(result?.sum ?? "0");
1352
508
  }
1353
- /**
1354
- * Average of field values.
1355
- */
509
+ /** AVG of a numeric field. */
1356
510
  async avg(field) {
1357
511
  this.applyPendingScopes();
1358
512
  const result = await this.selectRaw(`AVG(${field}) as avg`).first();
1359
513
  return parseFloat(result?.avg ?? "0");
1360
514
  }
1361
- /**
1362
- * Minimum field value.
1363
- */
515
+ /** MIN of a numeric field. */
1364
516
  async min(field) {
1365
517
  this.applyPendingScopes();
1366
518
  const result = await this.selectRaw(`MIN(${field}) as min`).first();
1367
519
  return parseFloat(result?.min ?? "0");
1368
520
  }
1369
- /**
1370
- * Maximum field value.
1371
- */
521
+ /** MAX of a numeric field. */
1372
522
  async max(field) {
1373
523
  this.applyPendingScopes();
1374
524
  const result = await this.selectRaw(`MAX(${field}) as max`).first();
1375
525
  return parseFloat(result?.max ?? "0");
1376
526
  }
1377
- /**
1378
- * Get distinct values.
1379
- */
527
+ /** Get distinct values for a field. */
1380
528
  async distinct(field) {
1381
529
  this.distinctValues(field);
1382
530
  const results = await this.get();
1383
531
  return results.map((row) => row[field]);
1384
532
  }
1385
- /**
1386
- * Get array of values for field.
1387
- */
533
+ /** Get array of all values for a single field. */
1388
534
  async pluck(field) {
1389
535
  const results = await this.select([field]).get();
1390
536
  return results.map((row) => row[field]);
1391
537
  }
1392
- /**
1393
- * Get single scalar value.
1394
- */
538
+ /** Get a single scalar value. */
1395
539
  async value(field) {
1396
540
  const result = await this.select([field]).first();
1397
541
  return result?.[field] ?? null;
1398
542
  }
1399
- /**
1400
- * Check if records exist.
1401
- */
543
+ /** Check whether any matching rows exist. */
1402
544
  async exists() {
1403
545
  const count = await this.limit(1).count();
1404
546
  return count > 0;
1405
547
  }
1406
- /**
1407
- * Check if no records exist.
1408
- */
548
+ /** Check whether NO matching rows exist. */
1409
549
  async notExists() {
1410
550
  return !(await this.exists());
1411
551
  }
1412
- /**
1413
- * Count distinct values.
1414
- */
552
+ /** COUNT DISTINCT a field. */
1415
553
  async countDistinct(field) {
1416
554
  const result = await this.selectRaw(`COUNT(DISTINCT ${field}) as count`).first();
1417
555
  return parseInt(result?.count ?? "0", 10);
1418
556
  }
1419
- /**
1420
- * Increment field value.
1421
- */
557
+ // ─── Aggregation shortcuts via latest/oldest ─────────────────
558
+ /** Get latest records ordered by a column. */
559
+ async latest(column = "createdAt") {
560
+ return this.orderBy(column, "desc").get();
561
+ }
562
+ // ─── Increment / Decrement ───────────────────────────────────
563
+ /** Increment a numeric field. Returns new value. */
1422
564
  async increment(field, amount = 1) {
1423
565
  this.applyPendingScopes();
1424
- const { sql, params } = this.buildFilter();
1425
- const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)} SET ${this.driver.dialect.quoteIdentifier(field)} = COALESCE(${this.driver.dialect.quoteIdentifier(field)}, 0) + $1 WHERE ${sql.replace("WHERE ", "")} RETURNING ${this.driver.dialect.quoteIdentifier(field)}`;
1426
- const result = await this.driver.query(updateSql, [amount, ...params]);
566
+ const { sql: filterSql, params: filterParams } = this.buildFilter();
567
+ const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)} ` +
568
+ `SET ${this.driver.dialect.quoteIdentifier(field)} = COALESCE(${this.driver.dialect.quoteIdentifier(field)}, 0) + $1 ` +
569
+ (filterSql ? `WHERE ${filterSql.replace("WHERE ", "")} ` : "") +
570
+ `RETURNING ${this.driver.dialect.quoteIdentifier(field)}`;
571
+ const result = await this.driver.query(updateSql, [
572
+ amount,
573
+ ...filterParams,
574
+ ]);
1427
575
  return result.rows[0]?.[field] ?? 0;
1428
576
  }
1429
- /**
1430
- * Decrement field value.
1431
- */
577
+ /** Decrement a numeric field. Returns new value. */
1432
578
  async decrement(field, amount = 1) {
1433
579
  return this.increment(field, -amount);
1434
580
  }
1435
- /**
1436
- * Increment for all matching.
1437
- */
581
+ /** Increment a field for all matching rows. Returns affected row count. */
1438
582
  async incrementMany(field, amount = 1) {
1439
583
  this.applyPendingScopes();
1440
- const { sql, params } = this.buildFilter();
1441
- const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)} SET ${this.driver.dialect.quoteIdentifier(field)} = COALESCE(${this.driver.dialect.quoteIdentifier(field)}, 0) + $1 WHERE ${sql.replace("WHERE ", "")}`;
1442
- const result = await this.driver.query(updateSql, [amount, ...params]);
584
+ const { sql: filterSql, params: filterParams } = this.buildFilter();
585
+ const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)} ` +
586
+ `SET ${this.driver.dialect.quoteIdentifier(field)} = COALESCE(${this.driver.dialect.quoteIdentifier(field)}, 0) + $1` +
587
+ (filterSql ? ` WHERE ${filterSql.replace("WHERE ", "")}` : "");
588
+ const result = await this.driver.query(updateSql, [amount, ...filterParams]);
1443
589
  return result.rowCount ?? 0;
1444
590
  }
1445
- /**
1446
- * Decrement for all matching.
1447
- */
591
+ /** Decrement a field for all matching rows. Returns affected row count. */
1448
592
  async decrementMany(field, amount = 1) {
1449
593
  return this.incrementMany(field, -amount);
1450
594
  }
1451
- // ============================================================================
1452
- // CHUNKING / PAGINATION
1453
- // ============================================================================
595
+ // ─── Chunking / Pagination ───────────────────────────────────
1454
596
  /**
1455
- * Process results in chunks.
597
+ * Process results in memory-efficient chunks.
598
+ *
599
+ * @example
600
+ * await User.query().chunk(100, async (rows, idx) => { ... })
1456
601
  */
1457
602
  async chunk(size, callback) {
1458
603
  let chunkIndex = 0;
@@ -1462,20 +607,16 @@ class PostgresQueryBuilder {
1462
607
  .skip(chunkIndex * size)
1463
608
  .limit(size)
1464
609
  .get();
1465
- if (chunk.length === 0) {
610
+ if (chunk.length === 0)
1466
611
  break;
1467
- }
1468
612
  const shouldContinue = await callback(chunk, chunkIndex);
1469
- if (shouldContinue === false) {
613
+ if (shouldContinue === false)
1470
614
  break;
1471
- }
1472
615
  hasMore = chunk.length === size;
1473
616
  chunkIndex++;
1474
617
  }
1475
618
  }
1476
- /**
1477
- * Page-based pagination.
1478
- */
619
+ /** Page-based pagination. */
1479
620
  async paginate(options) {
1480
621
  const page = options?.page ?? 1;
1481
622
  const limit = options?.limit ?? 10;
@@ -1495,22 +636,32 @@ class PostgresQueryBuilder {
1495
636
  };
1496
637
  }
1497
638
  /**
1498
- * Cursor-based pagination.
639
+ * Set cursor pagination hints fluently.
640
+ * The recorded values are picked up by `cursorPaginate()` when no explicit
641
+ * options are passed.
642
+ *
643
+ * @example
644
+ * User.query().cursor(lastId).cursorPaginate({ limit: 20 })
1499
645
  */
646
+ cursor(after, before) {
647
+ this.addOperation("cursor", { after, before });
648
+ return this;
649
+ }
650
+ /** Cursor-based pagination. */
1500
651
  async cursorPaginate(options) {
1501
- const { limit = 10, cursor, column = "id", direction = "next" } = options ?? {};
652
+ // Fall back to fluently-recorded cursor op if options.cursor not provided
653
+ const cursorOp = this.getOps("cursor")[0];
654
+ const recordedCursor = cursorOp?.data.after;
655
+ const { limit = 10, cursor = recordedCursor, column = "id", direction = "next", } = options ?? {};
1502
656
  if (cursor) {
1503
- const operator = direction === "next" ? ">" : "<";
1504
- this.where(column, operator, cursor);
657
+ this.where(column, direction === "next" ? ">" : "<", cursor);
1505
658
  }
1506
- const sortOrder = direction === "next" ? "asc" : "desc";
1507
- this.orderBy(column, sortOrder);
659
+ this.orderBy(column, direction === "next" ? "asc" : "desc");
1508
660
  const results = await this.limit(limit + 1).get();
1509
661
  const hasMore = results.length > limit;
1510
662
  let data = hasMore ? results.slice(0, limit) : results;
1511
- if (direction === "prev") {
663
+ if (direction === "prev")
1512
664
  data = data.reverse();
1513
- }
1514
665
  let nextCursor;
1515
666
  let prevCursor;
1516
667
  let hasPrev = false;
@@ -1527,27 +678,14 @@ class PostgresQueryBuilder {
1527
678
  else {
1528
679
  prevCursor = hasMore ? firstItem : undefined;
1529
680
  hasPrev = hasMore;
1530
- if (cursor) {
681
+ if (cursor)
1531
682
  nextCursor = lastItem;
1532
- }
1533
683
  }
1534
684
  }
1535
- return {
1536
- data,
1537
- pagination: {
1538
- hasMore,
1539
- hasPrev,
1540
- nextCursor,
1541
- prevCursor,
1542
- },
1543
- };
685
+ return { data, pagination: { hasMore, hasPrev, nextCursor, prevCursor } };
1544
686
  }
1545
- // ============================================================================
1546
- // MUTATION METHODS
1547
- // ============================================================================
1548
- /**
1549
- * Delete matching records.
1550
- */
687
+ // ─── Mutation methods ────────────────────────────────────────
688
+ /** Delete matching rows. Returns deleted count. */
1551
689
  async delete() {
1552
690
  this.applyPendingScopes();
1553
691
  const { sql, params } = this.buildFilter();
@@ -1555,246 +693,241 @@ class PostgresQueryBuilder {
1555
693
  const result = await this.driver.query(deleteSql, params);
1556
694
  return result.rowCount ?? 0;
1557
695
  }
1558
- /**
1559
- * Delete first matching record.
1560
- */
696
+ /** Delete the first matching row. */
1561
697
  async deleteOne() {
1562
698
  return this.limit(1).delete();
1563
699
  }
1564
- /**
1565
- * Update matching records.
1566
- */
700
+ /** Update matching rows. */
1567
701
  async update(fields) {
1568
702
  this.applyPendingScopes();
1569
703
  const result = await this.driver.updateMany(this.table, {}, { $set: fields });
1570
704
  return result.modifiedCount;
1571
705
  }
1572
- /**
1573
- * Unset fields from matching records.
1574
- */
706
+ /** Unset fields from matching rows. */
1575
707
  async unset(...fields) {
1576
708
  this.applyPendingScopes();
1577
709
  const updateObj = {};
1578
- for (const field of fields) {
710
+ for (const field of fields)
1579
711
  updateObj[field] = 1;
1580
- }
1581
712
  const result = await this.driver.updateMany(this.table, {}, { $unset: updateObj });
1582
713
  return result.modifiedCount;
1583
714
  }
1584
- // ============================================================================
1585
- // INSPECTION / DEBUGGING
1586
- // ============================================================================
1587
- /**
1588
- * Get the raw SQL query.
1589
- */
715
+ // ─── Inspection / Debugging ───────────────────────────────────
716
+ /** Return the SQL + bindings without executing. */
1590
717
  parse() {
1591
718
  this.applyPendingScopes();
1592
719
  const parser = new PostgresQueryParser({
1593
720
  table: this.table,
1594
- operations: this.operations,
721
+ operations: toParserOps(this.operations),
1595
722
  });
1596
723
  return parser.parse();
1597
724
  }
1598
- /**
1599
- * Get formatted SQL string.
1600
- */
725
+ /** Formatted SQL string (for logging/debugging). */
1601
726
  pretty() {
1602
- const { sql, params } = this.parse();
1603
- return `${sql}\n-- Parameters: ${JSON.stringify(params)}`;
727
+ const { query = "", bindings } = this.parse();
728
+ return `${query}\n-- Bindings: ${JSON.stringify(bindings ?? [])}`;
1604
729
  }
1605
- /**
1606
- * Get query execution plan.
1607
- */
730
+ /** Run EXPLAIN ANALYZE on the query. */
1608
731
  async explain() {
1609
- const { sql, params } = this.parse();
1610
- const explainSql = `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${sql}`;
1611
- const result = await this.driver.query(explainSql, params);
732
+ const { query = "", bindings = [] } = this.parse();
733
+ const result = await this.driver.query(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query}`, bindings);
1612
734
  return result.rows;
1613
735
  }
1614
- // ============================================================================
1615
- // UTILITY METHODS
1616
- // ============================================================================
1617
- /**
1618
- * Add driver-specific raw modifications to the query.
1619
- */
1620
- raw(builder) {
1621
- // For PostgreSQL, the native object would be the operations array
1622
- builder(this.operations);
1623
- return this;
1624
- }
1625
- /**
1626
- * Extend the query builder with driver-specific extensions.
1627
- */
736
+ // ─── Utility ──────────────────────────────────────────────────
737
+ /** Extend the builder with a driver-specific extension. */
1628
738
  extend(extension, ..._args) {
1629
- // PostgreSQL doesn't have driver-specific extensions like MongoDB
1630
- // Return undefined as R for now - specific extensions can be added later
1631
739
  throw new Error(`Extension "${extension}" is not supported by PostgresQueryBuilder`);
1632
740
  }
1633
- /**
1634
- * Tap into the query builder for side-effects.
1635
- */
1636
- tap(callback) {
1637
- callback(this);
1638
- return this;
1639
- }
1640
- /**
1641
- * Conditionally apply query modifications.
1642
- */
1643
- when(condition, callback, otherwise) {
1644
- if (condition) {
1645
- callback(this, condition);
1646
- }
1647
- else if (otherwise) {
1648
- otherwise(this);
1649
- }
1650
- return this;
741
+ /** Pluck scalar values for a single field (alias for pluck). */
742
+ async pluckOne(field) {
743
+ const results = await this.select([field]).get();
744
+ return results.map((row) => row[field]);
1651
745
  }
1652
746
  // ============================================================================
1653
- // INTERNAL HELPERS
747
+ // JOIN RELATIONS — INTERNAL PIPELINE
1654
748
  // ============================================================================
1655
749
  /**
1656
- * Build WHERE clause from current operations.
750
+ * Before `get()` runs the parser, consume any joinWith ops recorded by the base
751
+ * class and expand them into the joinRelations Map.
1657
752
  */
1658
- buildFilter() {
1659
- const whereOps = this.operations.filter((op) => op.type.includes("where") || op.type.includes("Where"));
1660
- if (whereOps.length === 0) {
1661
- return { sql: "", params: [] };
753
+ _processJoinWithOps() {
754
+ const joinWithOps = this.operations.filter((op) => op.type === "joinWith");
755
+ if (joinWithOps.length === 0)
756
+ return;
757
+ // Remove joinWith ops from main operations — they are consumed here
758
+ this.operations = this.operations.filter((op) => op.type !== "joinWith");
759
+ for (const op of joinWithOps) {
760
+ const constraints = op.data.constraints;
761
+ for (const [path, constraint] of Object.entries(constraints)) {
762
+ // Re-delegate to the extended joinWith implementation
763
+ if (!constraint || constraint === "") {
764
+ this.joinWith(path);
765
+ }
766
+ else {
767
+ this.joinWith({ [path]: constraint });
768
+ }
769
+ }
1662
770
  }
1663
- const parser = new PostgresQueryParser({
1664
- table: this.table,
1665
- operations: whereOps,
1666
- });
1667
- const { sql, params } = parser.parse();
1668
- const whereMatch = sql.match(/WHERE .+$/);
1669
- return {
1670
- sql: whereMatch ? whereMatch[0] : "",
1671
- params,
1672
- };
1673
771
  }
1674
- // ============================================================================
1675
- // RELATIONS / EAGER LOADING (Stubs)
1676
- // ============================================================================
1677
- /**
1678
- * Relations to eagerly load.
1679
- */
1680
- eagerLoadRelations = new Map();
1681
- /**
1682
- * Relations to count.
1683
- */
1684
- countRelations = [];
1685
- /**
1686
- * Relations to load via JOIN (single query).
1687
- */
1688
- joinRelations = new Map();
1689
- /**
1690
- * Relation definitions from the model.
1691
- */
1692
- relationDefinitions;
1693
- /**
1694
- * Model class reference.
1695
- */
1696
- modelClass;
1697
772
  /**
1698
- * Load relations using database JOINs in a single query.
1699
- *
1700
- * Unlike `with()` which uses separate queries, `joinWith()` uses
1701
- * LEFT JOIN to fetch related data in a single query.
1702
- *
1703
- * @param relations - Relation names to load via JOIN
1704
- * @returns This builder for chaining
773
+ * Translate each entry in `joinRelations` into actual JOIN + selectRelatedColumns operations.
1705
774
  */
1706
- joinWith(...relations) {
1707
- for (const relation of relations) {
1708
- const def = this.relationDefinitions?.[relation];
1709
- if (def) {
1710
- this.joinRelations.set(relation, {
1711
- alias: `_rel_${relation}`,
1712
- type: def.type,
775
+ applyJoinRelations() {
776
+ if (this.joinRelations.size === 0)
777
+ return;
778
+ for (const [path, config] of this.joinRelations) {
779
+ const RelatedModel = typeof config.model === "string"
780
+ ? getModelFromRegistry(config.model)
781
+ : config.model;
782
+ if (!RelatedModel) {
783
+ throw new Error(`Relation model not found for ${path}`);
784
+ }
785
+ const relatedTable = RelatedModel.table;
786
+ const alias = config.alias;
787
+ const parentTable = config.parentPath
788
+ ? this.joinRelations.get(config.parentPath).alias
789
+ : this.table;
790
+ const parentDefTable = config.parentModel?.table ?? this.table;
791
+ let localField;
792
+ let foreignField;
793
+ if (config.type === "belongsTo") {
794
+ localField = config.foreignKey ?? `${config.relationName}Id`;
795
+ foreignField = config.ownerKey ?? "id";
796
+ }
797
+ else {
798
+ localField = config.localKey ?? "id";
799
+ foreignField = config.foreignKey ?? `${parentDefTable.slice(0, -1)}Id`;
800
+ }
801
+ // hasMany uses a correlated subquery in SELECT (no JOIN) to avoid row explosion
802
+ if (config.type !== "hasMany") {
803
+ this.addOperation("leftJoin", {
804
+ table: relatedTable,
805
+ alias,
806
+ localField: `${parentTable}.${localField}`,
807
+ foreignField,
1713
808
  });
1714
809
  }
810
+ this.addOperation("selectRelatedColumns", {
811
+ alias,
812
+ relationName: config.relationName,
813
+ path,
814
+ table: relatedTable,
815
+ select: config.select,
816
+ type: config.type,
817
+ foreignKey: foreignField,
818
+ localKey: localField,
819
+ parentTable,
820
+ constraintOps: config.constraintOps, // passed through to parser
821
+ });
1715
822
  }
1716
- return this;
1717
823
  }
1718
824
  /**
1719
- * Eagerly load one or more relations.
1720
- *
1721
- * Supported patterns:
1722
- * - `with("posts")` - Load relation
1723
- * - `with("posts", "comments")` - Load multiple relations
1724
- * - `with("posts", callback)` - Load relation with constraint
1725
- * - `with({ posts: true, comments: callback })` - Object configuration
1726
- *
1727
- * @param args - Relation name(s), callbacks, or configuration object
825
+ * Extract per-relation data from raw DB rows (before hydration).
826
+ * Returns a Map of row index → nested relation data tree.
1728
827
  */
1729
- with(...args) {
1730
- for (let i = 0; i < args.length; i++) {
1731
- const arg = args[i];
1732
- if (typeof arg === "string") {
1733
- // Check if next argument is a callback for this relation
1734
- const nextArg = args[i + 1];
1735
- if (typeof nextArg === "function") {
1736
- this.eagerLoadRelations.set(arg, nextArg);
1737
- i++; // Skip the callback in next iteration
1738
- }
1739
- else {
1740
- this.eagerLoadRelations.set(arg, true);
1741
- }
1742
- }
1743
- else if (typeof arg === "object" && arg !== null) {
1744
- for (const [key, value] of Object.entries(arg)) {
1745
- this.eagerLoadRelations.set(key, value);
828
+ extractJoinedRelationData(records) {
829
+ const result = new Map();
830
+ if (this.joinRelations.size === 0)
831
+ return result;
832
+ records.forEach((record, index) => {
833
+ const relationData = {};
834
+ // Process shallower paths first so parents exist before children
835
+ const sortedPaths = Array.from(this.joinRelations.keys()).sort((a, b) => a.split(".").length - b.split(".").length);
836
+ for (const path of sortedPaths) {
837
+ const config = this.joinRelations.get(path);
838
+ const columnName = config.alias;
839
+ const relatedData = record[columnName];
840
+ delete record[columnName];
841
+ const parsedData = relatedData !== null &&
842
+ !(typeof relatedData === "object" &&
843
+ Object.values(relatedData).every((v) => v === null))
844
+ ? relatedData
845
+ : null;
846
+ const parts = path.split(".");
847
+ const lastPart = parts.pop();
848
+ let current = relationData;
849
+ for (const part of parts) {
850
+ if (!current[part])
851
+ current[part] = {};
852
+ current = current[part];
1746
853
  }
854
+ current[lastPart] = parsedData;
1747
855
  }
1748
- // Functions not preceded by a string are ignored (invalid usage)
1749
- }
1750
- return this;
1751
- }
1752
- /**
1753
- * Add a count of related models as a virtual field.
1754
- * @param relations - Relation name(s) to count
1755
- */
1756
- withCount(...relations) {
1757
- this.countRelations.push(...relations);
1758
- return this;
1759
- }
1760
- /**
1761
- * Filter results to only those that have related models.
1762
- * @param relation - Relation name
1763
- * @param operator - Optional comparison operator
1764
- * @param count - Optional count to compare against
1765
- */
1766
- has(relation, operator, count) {
1767
- // TODO: Implement has() using EXISTS subquery
1768
- this.addOperation("has", { relation, operator, count });
1769
- return this;
1770
- }
1771
- /**
1772
- * Filter results that have related models matching specific conditions.
1773
- * @param relation - Relation name
1774
- * @param callback - Callback to define conditions
1775
- */
1776
- whereHas(relation, callback) {
1777
- // TODO: Implement whereHas() using EXISTS subquery with conditions
1778
- this.addOperation("whereHas", { relation, callback });
1779
- return this;
856
+ result.set(index, relationData);
857
+ });
858
+ return result;
1780
859
  }
1781
860
  /**
1782
- * Filter results that don't have any related models.
1783
- * @param relation - Relation name
861
+ * Attach extracted relation data to hydrated model instances.
1784
862
  */
1785
- doesntHave(relation) {
1786
- // TODO: Implement doesntHave() using NOT EXISTS subquery
1787
- this.addOperation("doesntHave", { relation });
1788
- return this;
863
+ attachJoinedRelations(records, joinedData) {
864
+ if (this.joinRelations.size === 0)
865
+ return;
866
+ const attachNested = (model, dataTree, currentPath = "") => {
867
+ if (!dataTree || typeof dataTree !== "object")
868
+ return;
869
+ for (const [key, data] of Object.entries(dataTree)) {
870
+ const path = currentPath ? `${currentPath}.${key}` : key;
871
+ const config = this.joinRelations.get(path);
872
+ if (!config)
873
+ continue;
874
+ const m = model;
875
+ if (data === null) {
876
+ m[key] = null;
877
+ m.loadedRelations?.set(key, null);
878
+ continue;
879
+ }
880
+ const RelatedModel = resolveModelClass(config.model);
881
+ if (!RelatedModel)
882
+ continue;
883
+ const childKeys = Array.from(this.joinRelations.keys())
884
+ .filter((p) => p.startsWith(`${path}.`))
885
+ .map((p) => p.split(".")[path.split(".").length]);
886
+ if (config.type === "hasMany") {
887
+ const rows = Array.isArray(data) ? data : [];
888
+ const instances = rows.map((row) => {
889
+ const rowData = { ...row };
890
+ for (const childKey of childKeys)
891
+ delete rowData[childKey];
892
+ return RelatedModel.hydrate(rowData);
893
+ });
894
+ m[key] = instances;
895
+ m.loadedRelations?.set(key, instances);
896
+ }
897
+ else {
898
+ const modelData = { ...data };
899
+ for (const childKey of childKeys)
900
+ delete modelData[childKey];
901
+ const relatedInstance = RelatedModel.hydrate(modelData);
902
+ attachNested(relatedInstance, data, path);
903
+ m[key] = relatedInstance;
904
+ m.loadedRelations?.set(key, relatedInstance);
905
+ }
906
+ }
907
+ };
908
+ records.forEach((model, index) => {
909
+ const relationData = joinedData.get(index);
910
+ if (relationData)
911
+ attachNested(model, relationData);
912
+ });
1789
913
  }
914
+ // ============================================================================
915
+ // INTERNAL HELPERS
916
+ // ============================================================================
1790
917
  /**
1791
- * Filter results that don't have related models matching specific conditions.
1792
- * @param relation - Relation name
1793
- * @param callback - Callback to define conditions
918
+ * Build a WHERE-only SQL fragment from `where*` operations on the current builder.
919
+ * Used by DELETE / UPDATE / increment paths.
1794
920
  */
1795
- whereDoesntHave(relation, callback) {
1796
- // TODO: Implement whereDoesntHave() using NOT EXISTS subquery with conditions
1797
- this.addOperation("whereDoesntHave", { relation, callback });
1798
- return this;
921
+ buildFilter() {
922
+ const whereOps = this.operations.filter((op) => op.type.includes("where") || op.type.includes("Where"));
923
+ if (whereOps.length === 0)
924
+ return { sql: "", params: [] };
925
+ const parser = new PostgresQueryParser({
926
+ table: this.table,
927
+ operations: toParserOps(whereOps),
928
+ });
929
+ const { query = "", bindings = [] } = parser.parse();
930
+ const whereMatch = query.match(/WHERE .+$/);
931
+ return { sql: whereMatch ? whereMatch[0] : "", params: bindings };
1799
932
  }
1800
933
  }export{PostgresQueryBuilder};//# sourceMappingURL=postgres-query-builder.js.map