@open-mercato/core 0.5.1-develop.2691.d8a0934b37 → 0.5.1-develop.2694.732417c5ec

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 (414) hide show
  1. package/dist/modules/api_keys/data/entities.js +1 -1
  2. package/dist/modules/api_keys/data/entities.js.map +1 -1
  3. package/dist/modules/api_keys/services/apiKeyService.js +5 -5
  4. package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
  5. package/dist/modules/attachments/api/library/[id]/route.js +1 -1
  6. package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
  7. package/dist/modules/attachments/api/library/route.js +7 -9
  8. package/dist/modules/attachments/api/library/route.js.map +2 -2
  9. package/dist/modules/attachments/api/partitions/route.js +3 -3
  10. package/dist/modules/attachments/api/partitions/route.js.map +2 -2
  11. package/dist/modules/attachments/api/route.js +6 -5
  12. package/dist/modules/attachments/api/route.js.map +2 -2
  13. package/dist/modules/attachments/api/transfer/route.js +1 -1
  14. package/dist/modules/attachments/api/transfer/route.js.map +2 -2
  15. package/dist/modules/attachments/data/entities.js +2 -1
  16. package/dist/modules/attachments/data/entities.js.map +2 -2
  17. package/dist/modules/attachments/lib/ocrQueue.js +1 -1
  18. package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
  19. package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js.map +2 -2
  20. package/dist/modules/audit_logs/api/audit-logs/actions/route.js.map +2 -2
  21. package/dist/modules/audit_logs/data/entities.js +1 -1
  22. package/dist/modules/audit_logs/data/entities.js.map +1 -1
  23. package/dist/modules/audit_logs/services/actionLogService.js +77 -70
  24. package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
  25. package/dist/modules/auth/api/roles/acl/route.js +1 -1
  26. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  27. package/dist/modules/auth/api/users/acl/route.js +2 -2
  28. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  29. package/dist/modules/auth/api/users/resend-invite/route.js +1 -1
  30. package/dist/modules/auth/api/users/resend-invite/route.js.map +2 -2
  31. package/dist/modules/auth/cli.js +12 -6
  32. package/dist/modules/auth/cli.js.map +2 -2
  33. package/dist/modules/auth/commands/users.js +1 -1
  34. package/dist/modules/auth/commands/users.js.map +2 -2
  35. package/dist/modules/auth/data/entities.js +1 -1
  36. package/dist/modules/auth/data/entities.js.map +2 -2
  37. package/dist/modules/auth/lib/setup-app.js +3 -3
  38. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  39. package/dist/modules/auth/services/authService.js +2 -2
  40. package/dist/modules/auth/services/authService.js.map +2 -2
  41. package/dist/modules/business_rules/api/rules/route.js +3 -3
  42. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  43. package/dist/modules/business_rules/api/sets/[id]/members/route.js +7 -4
  44. package/dist/modules/business_rules/api/sets/[id]/members/route.js.map +2 -2
  45. package/dist/modules/business_rules/api/sets/route.js +3 -3
  46. package/dist/modules/business_rules/api/sets/route.js.map +2 -2
  47. package/dist/modules/business_rules/cli.js +1 -1
  48. package/dist/modules/business_rules/cli.js.map +2 -2
  49. package/dist/modules/business_rules/data/entities.js +2 -9
  50. package/dist/modules/business_rules/data/entities.js.map +2 -2
  51. package/dist/modules/business_rules/lib/rule-engine.js +1 -1
  52. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  53. package/dist/modules/catalog/api/option-schemas/route.js +0 -1
  54. package/dist/modules/catalog/api/option-schemas/route.js.map +2 -2
  55. package/dist/modules/catalog/data/entities.js +2 -11
  56. package/dist/modules/catalog/data/entities.js.map +2 -2
  57. package/dist/modules/configs/data/entities.js +2 -1
  58. package/dist/modules/configs/data/entities.js.map +2 -2
  59. package/dist/modules/currencies/commands/fetch-configs.js +3 -3
  60. package/dist/modules/currencies/commands/fetch-configs.js.map +2 -2
  61. package/dist/modules/currencies/data/entities.js +1 -1
  62. package/dist/modules/currencies/data/entities.js.map +2 -2
  63. package/dist/modules/customer_accounts/api/signup.js +1 -1
  64. package/dist/modules/customer_accounts/api/signup.js.map +2 -2
  65. package/dist/modules/customer_accounts/data/entities.js +1 -1
  66. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  67. package/dist/modules/customer_accounts/services/customerInvitationService.js +1 -1
  68. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  69. package/dist/modules/customer_accounts/services/customerSessionService.js +1 -1
  70. package/dist/modules/customer_accounts/services/customerSessionService.js.map +2 -2
  71. package/dist/modules/customer_accounts/services/customerTokenService.js +12 -7
  72. package/dist/modules/customer_accounts/services/customerTokenService.js.map +2 -2
  73. package/dist/modules/customers/api/interactions/conflicts/route.js +19 -17
  74. package/dist/modules/customers/api/interactions/conflicts/route.js.map +2 -2
  75. package/dist/modules/customers/api/interactions/counts/route.js +7 -6
  76. package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
  77. package/dist/modules/customers/api/interactions/route.js +28 -42
  78. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  79. package/dist/modules/customers/api/utils.js +29 -24
  80. package/dist/modules/customers/api/utils.js.map +2 -2
  81. package/dist/modules/customers/cli.js +45 -40
  82. package/dist/modules/customers/cli.js.map +2 -2
  83. package/dist/modules/customers/commands/dictionaries.js +1 -1
  84. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  85. package/dist/modules/customers/commands/tags.js +1 -1
  86. package/dist/modules/customers/commands/tags.js.map +2 -2
  87. package/dist/modules/customers/data/entities.js +2 -12
  88. package/dist/modules/customers/data/entities.js.map +2 -2
  89. package/dist/modules/customers/lib/interactionProjection.js +18 -15
  90. package/dist/modules/customers/lib/interactionProjection.js.map +2 -2
  91. package/dist/modules/customers/lib/personCompanyLinkTable.js +6 -8
  92. package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
  93. package/dist/modules/dashboards/api/roles/widgets/route.js +1 -1
  94. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  95. package/dist/modules/dashboards/api/users/widgets/route.js +1 -1
  96. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  97. package/dist/modules/dashboards/data/entities.js +1 -1
  98. package/dist/modules/dashboards/data/entities.js.map +1 -1
  99. package/dist/modules/data_sync/api/mappings/route.js +1 -1
  100. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  101. package/dist/modules/data_sync/data/entities.js +2 -1
  102. package/dist/modules/data_sync/data/entities.js.map +2 -2
  103. package/dist/modules/data_sync/lib/id-mapping.js +1 -1
  104. package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
  105. package/dist/modules/data_sync/lib/sync-run-service.js +1 -1
  106. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  107. package/dist/modules/dictionaries/commands/factory.js +1 -1
  108. package/dist/modules/dictionaries/commands/factory.js.map +2 -2
  109. package/dist/modules/dictionaries/data/entities.js +2 -9
  110. package/dist/modules/dictionaries/data/entities.js.map +2 -2
  111. package/dist/modules/directory/commands/organizations.js +4 -4
  112. package/dist/modules/directory/commands/organizations.js.map +2 -2
  113. package/dist/modules/directory/data/entities.js +2 -1
  114. package/dist/modules/directory/data/entities.js.map +2 -2
  115. package/dist/modules/entities/api/definitions.js +2 -2
  116. package/dist/modules/entities/api/definitions.js.map +2 -2
  117. package/dist/modules/entities/api/encryption.js +2 -2
  118. package/dist/modules/entities/api/encryption.js.map +2 -2
  119. package/dist/modules/entities/api/relations/options.js +2 -2
  120. package/dist/modules/entities/api/relations/options.js.map +2 -2
  121. package/dist/modules/entities/cli.js +4 -4
  122. package/dist/modules/entities/cli.js.map +2 -2
  123. package/dist/modules/entities/data/entities.js +1 -1
  124. package/dist/modules/entities/data/entities.js.map +2 -2
  125. package/dist/modules/entities/lib/field-definitions.js +2 -2
  126. package/dist/modules/entities/lib/field-definitions.js.map +2 -2
  127. package/dist/modules/entities/lib/register.js +1 -1
  128. package/dist/modules/entities/lib/register.js.map +2 -2
  129. package/dist/modules/feature_toggles/data/entities.js +2 -9
  130. package/dist/modules/feature_toggles/data/entities.js.map +2 -2
  131. package/dist/modules/inbox_ops/api/proposals/counts/route.js +3 -6
  132. package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
  133. package/dist/modules/inbox_ops/data/entities.js +2 -8
  134. package/dist/modules/inbox_ops/data/entities.js.map +2 -2
  135. package/dist/modules/inbox_ops/lib/messagesIntegration.js +6 -6
  136. package/dist/modules/inbox_ops/lib/messagesIntegration.js.map +2 -2
  137. package/dist/modules/integrations/data/entities.js +2 -1
  138. package/dist/modules/integrations/data/entities.js.map +2 -2
  139. package/dist/modules/integrations/lib/credentials-service.js +1 -1
  140. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  141. package/dist/modules/integrations/lib/log-service.js +1 -1
  142. package/dist/modules/integrations/lib/log-service.js.map +2 -2
  143. package/dist/modules/integrations/lib/state-service.js +1 -1
  144. package/dist/modules/integrations/lib/state-service.js.map +2 -2
  145. package/dist/modules/messages/api/route.js +90 -93
  146. package/dist/modules/messages/api/route.js.map +2 -2
  147. package/dist/modules/messages/api/unread-count/route.js +8 -7
  148. package/dist/modules/messages/api/unread-count/route.js.map +2 -2
  149. package/dist/modules/messages/commands/confirmations.js +1 -1
  150. package/dist/modules/messages/commands/confirmations.js.map +2 -2
  151. package/dist/modules/messages/commands/messages.js +3 -3
  152. package/dist/modules/messages/commands/messages.js.map +2 -2
  153. package/dist/modules/messages/data/entities.js +2 -1
  154. package/dist/modules/messages/data/entities.js.map +2 -2
  155. package/dist/modules/messages/lib/email-sender.js +1 -1
  156. package/dist/modules/messages/lib/email-sender.js.map +2 -2
  157. package/dist/modules/messages/lib/searchLookup.js +8 -8
  158. package/dist/modules/messages/lib/searchLookup.js.map +2 -2
  159. package/dist/modules/messages/lib/tokenConsumption.js +9 -4
  160. package/dist/modules/messages/lib/tokenConsumption.js.map +2 -2
  161. package/dist/modules/notifications/data/entities.js +2 -1
  162. package/dist/modules/notifications/data/entities.js.map +2 -2
  163. package/dist/modules/notifications/lib/notificationRecipients.js +15 -5
  164. package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
  165. package/dist/modules/notifications/lib/notificationService.js +39 -34
  166. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  167. package/dist/modules/notifications/workers/create-notification.worker.js +14 -13
  168. package/dist/modules/notifications/workers/create-notification.worker.js.map +2 -2
  169. package/dist/modules/payment_gateways/api/transactions/route.js +2 -2
  170. package/dist/modules/payment_gateways/api/transactions/route.js.map +2 -2
  171. package/dist/modules/payment_gateways/data/entities.js +2 -1
  172. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  173. package/dist/modules/payment_gateways/lib/gateway-service.js +1 -1
  174. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  175. package/dist/modules/payment_gateways/lib/webhook-utils.js +2 -2
  176. package/dist/modules/payment_gateways/lib/webhook-utils.js.map +2 -2
  177. package/dist/modules/perspectives/data/entities.js +1 -1
  178. package/dist/modules/perspectives/data/entities.js.map +2 -2
  179. package/dist/modules/planner/data/entities.js +1 -1
  180. package/dist/modules/planner/data/entities.js.map +2 -2
  181. package/dist/modules/progress/data/entities.js +2 -1
  182. package/dist/modules/progress/data/entities.js.map +2 -2
  183. package/dist/modules/progress/lib/progressServiceImpl.js +1 -1
  184. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  185. package/dist/modules/query_index/api/status.js +66 -57
  186. package/dist/modules/query_index/api/status.js.map +2 -2
  187. package/dist/modules/query_index/cli.js +39 -24
  188. package/dist/modules/query_index/cli.js.map +2 -2
  189. package/dist/modules/query_index/data/entities.js +1 -1
  190. package/dist/modules/query_index/data/entities.js.map +2 -2
  191. package/dist/modules/query_index/di.js +25 -13
  192. package/dist/modules/query_index/di.js.map +2 -2
  193. package/dist/modules/query_index/lib/batch.js +31 -33
  194. package/dist/modules/query_index/lib/batch.js.map +2 -2
  195. package/dist/modules/query_index/lib/coverage.js +63 -50
  196. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  197. package/dist/modules/query_index/lib/engine.js +592 -588
  198. package/dist/modules/query_index/lib/engine.js.map +2 -2
  199. package/dist/modules/query_index/lib/indexer.js +74 -47
  200. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  201. package/dist/modules/query_index/lib/jobs.js +37 -24
  202. package/dist/modules/query_index/lib/jobs.js.map +2 -2
  203. package/dist/modules/query_index/lib/purge.js +19 -11
  204. package/dist/modules/query_index/lib/purge.js.map +2 -2
  205. package/dist/modules/query_index/lib/reindexer.js +47 -44
  206. package/dist/modules/query_index/lib/reindexer.js.map +2 -2
  207. package/dist/modules/query_index/lib/search-tokens.js +47 -25
  208. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  209. package/dist/modules/query_index/lib/stale.js +14 -12
  210. package/dist/modules/query_index/lib/stale.js.map +2 -2
  211. package/dist/modules/query_index/lib/subscriber-scope.js +2 -2
  212. package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
  213. package/dist/modules/query_index/subscribers/delete_one.js +3 -2
  214. package/dist/modules/query_index/subscribers/delete_one.js.map +2 -2
  215. package/dist/modules/resources/commands/tag-assignments.js +1 -1
  216. package/dist/modules/resources/commands/tag-assignments.js.map +2 -2
  217. package/dist/modules/resources/commands/tags.js +1 -1
  218. package/dist/modules/resources/commands/tags.js.map +2 -2
  219. package/dist/modules/resources/data/entities.js +2 -1
  220. package/dist/modules/resources/data/entities.js.map +2 -2
  221. package/dist/modules/sales/commands/documentAddresses.js +2 -2
  222. package/dist/modules/sales/commands/documentAddresses.js.map +2 -2
  223. package/dist/modules/sales/commands/notes.js.map +2 -2
  224. package/dist/modules/sales/commands/tags.js +1 -1
  225. package/dist/modules/sales/commands/tags.js.map +2 -2
  226. package/dist/modules/sales/data/enrichers.js +9 -8
  227. package/dist/modules/sales/data/enrichers.js.map +2 -2
  228. package/dist/modules/sales/data/entities.js +2 -11
  229. package/dist/modules/sales/data/entities.js.map +2 -2
  230. package/dist/modules/shipping_carriers/data/entities.js +2 -1
  231. package/dist/modules/shipping_carriers/data/entities.js.map +2 -2
  232. package/dist/modules/shipping_carriers/lib/shipping-service.js +1 -1
  233. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  234. package/dist/modules/shipping_carriers/lib/webhook-utils.js +2 -2
  235. package/dist/modules/shipping_carriers/lib/webhook-utils.js.map +2 -2
  236. package/dist/modules/staff/data/entities.js +1 -1
  237. package/dist/modules/staff/data/entities.js.map +2 -2
  238. package/dist/modules/translations/api/[entityType]/[entityId]/route.js +3 -5
  239. package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
  240. package/dist/modules/translations/api/context.js +2 -2
  241. package/dist/modules/translations/api/context.js.map +2 -2
  242. package/dist/modules/translations/commands/translations.js +46 -39
  243. package/dist/modules/translations/commands/translations.js.map +2 -2
  244. package/dist/modules/translations/components/TranslationManager.js +19 -10
  245. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  246. package/dist/modules/translations/data/entities.js +1 -1
  247. package/dist/modules/translations/data/entities.js.map +2 -2
  248. package/dist/modules/translations/lib/apply.js +4 -4
  249. package/dist/modules/translations/lib/apply.js.map +2 -2
  250. package/dist/modules/translations/lib/batch.js +3 -2
  251. package/dist/modules/translations/lib/batch.js.map +2 -2
  252. package/dist/modules/translations/subscribers/cleanup.js +3 -5
  253. package/dist/modules/translations/subscribers/cleanup.js.map +2 -2
  254. package/dist/modules/workflows/api/definitions/route.js +1 -1
  255. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  256. package/dist/modules/workflows/cli.js +5 -5
  257. package/dist/modules/workflows/cli.js.map +2 -2
  258. package/dist/modules/workflows/data/entities.js +2 -1
  259. package/dist/modules/workflows/data/entities.js.map +2 -2
  260. package/dist/modules/workflows/lib/event-logger.js +2 -2
  261. package/dist/modules/workflows/lib/event-logger.js.map +2 -2
  262. package/dist/modules/workflows/lib/seeds.js +16 -1
  263. package/dist/modules/workflows/lib/seeds.js.map +2 -2
  264. package/dist/modules/workflows/lib/step-handler.js +3 -3
  265. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  266. package/dist/modules/workflows/lib/task-handler.js +1 -1
  267. package/dist/modules/workflows/lib/task-handler.js.map +2 -2
  268. package/dist/modules/workflows/lib/transition-handler.js +1 -1
  269. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  270. package/dist/modules/workflows/lib/workflow-executor.js +2 -2
  271. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  272. package/jest.config.cjs +4 -2
  273. package/package.json +3 -3
  274. package/src/modules/api_keys/data/entities.ts +1 -1
  275. package/src/modules/api_keys/services/apiKeyService.ts +5 -5
  276. package/src/modules/attachments/api/library/[id]/route.ts +1 -1
  277. package/src/modules/attachments/api/library/route.ts +10 -12
  278. package/src/modules/attachments/api/partitions/route.ts +3 -3
  279. package/src/modules/attachments/api/route.ts +10 -8
  280. package/src/modules/attachments/api/transfer/route.ts +1 -1
  281. package/src/modules/attachments/data/entities.ts +2 -1
  282. package/src/modules/attachments/lib/ocrQueue.ts +1 -1
  283. package/src/modules/audit_logs/api/audit-logs/actions/export/route.ts +4 -4
  284. package/src/modules/audit_logs/api/audit-logs/actions/route.ts +4 -4
  285. package/src/modules/audit_logs/data/entities.ts +1 -1
  286. package/src/modules/audit_logs/services/actionLogService.ts +96 -87
  287. package/src/modules/auth/api/roles/acl/route.ts +1 -1
  288. package/src/modules/auth/api/users/acl/route.ts +2 -2
  289. package/src/modules/auth/api/users/resend-invite/route.ts +1 -1
  290. package/src/modules/auth/cli.ts +46 -40
  291. package/src/modules/auth/commands/users.ts +1 -1
  292. package/src/modules/auth/data/entities.ts +1 -1
  293. package/src/modules/auth/lib/setup-app.ts +3 -3
  294. package/src/modules/auth/services/authService.ts +2 -2
  295. package/src/modules/business_rules/api/rules/route.ts +3 -3
  296. package/src/modules/business_rules/api/sets/[id]/members/route.ts +7 -4
  297. package/src/modules/business_rules/api/sets/route.ts +3 -3
  298. package/src/modules/business_rules/cli.ts +1 -1
  299. package/src/modules/business_rules/data/entities.ts +2 -9
  300. package/src/modules/business_rules/lib/rule-engine.ts +1 -1
  301. package/src/modules/catalog/api/option-schemas/route.ts +0 -1
  302. package/src/modules/catalog/data/entities.ts +2 -11
  303. package/src/modules/configs/data/entities.ts +2 -1
  304. package/src/modules/currencies/commands/fetch-configs.ts +3 -3
  305. package/src/modules/currencies/data/entities.ts +1 -1
  306. package/src/modules/customer_accounts/api/signup.ts +1 -1
  307. package/src/modules/customer_accounts/data/entities.ts +1 -1
  308. package/src/modules/customer_accounts/services/customerInvitationService.ts +1 -1
  309. package/src/modules/customer_accounts/services/customerSessionService.ts +1 -1
  310. package/src/modules/customer_accounts/services/customerTokenService.ts +26 -15
  311. package/src/modules/customers/api/interactions/conflicts/route.ts +26 -23
  312. package/src/modules/customers/api/interactions/counts/route.ts +13 -11
  313. package/src/modules/customers/api/interactions/route.ts +32 -44
  314. package/src/modules/customers/api/utils.ts +45 -37
  315. package/src/modules/customers/cli.ts +88 -67
  316. package/src/modules/customers/commands/dictionaries.ts +1 -1
  317. package/src/modules/customers/commands/tags.ts +1 -1
  318. package/src/modules/customers/data/entities.ts +2 -12
  319. package/src/modules/customers/lib/interactionProjection.ts +36 -25
  320. package/src/modules/customers/lib/personCompanyLinkTable.ts +13 -18
  321. package/src/modules/dashboards/api/roles/widgets/route.ts +1 -1
  322. package/src/modules/dashboards/api/users/widgets/route.ts +1 -1
  323. package/src/modules/dashboards/data/entities.ts +1 -1
  324. package/src/modules/data_sync/api/mappings/route.ts +1 -1
  325. package/src/modules/data_sync/data/entities.ts +2 -1
  326. package/src/modules/data_sync/lib/id-mapping.ts +1 -1
  327. package/src/modules/data_sync/lib/sync-run-service.ts +1 -1
  328. package/src/modules/dictionaries/commands/factory.ts +1 -1
  329. package/src/modules/dictionaries/data/entities.ts +2 -9
  330. package/src/modules/directory/commands/organizations.ts +4 -4
  331. package/src/modules/directory/data/entities.ts +2 -1
  332. package/src/modules/entities/api/definitions.ts +2 -2
  333. package/src/modules/entities/api/encryption.ts +2 -2
  334. package/src/modules/entities/api/relations/options.ts +8 -3
  335. package/src/modules/entities/cli.ts +4 -4
  336. package/src/modules/entities/data/entities.ts +1 -1
  337. package/src/modules/entities/lib/field-definitions.ts +2 -2
  338. package/src/modules/entities/lib/register.ts +1 -1
  339. package/src/modules/feature_toggles/data/entities.ts +2 -9
  340. package/src/modules/inbox_ops/api/proposals/counts/route.ts +10 -10
  341. package/src/modules/inbox_ops/data/entities.ts +2 -8
  342. package/src/modules/inbox_ops/lib/messagesIntegration.ts +12 -11
  343. package/src/modules/integrations/data/entities.ts +2 -1
  344. package/src/modules/integrations/lib/credentials-service.ts +1 -1
  345. package/src/modules/integrations/lib/log-service.ts +1 -1
  346. package/src/modules/integrations/lib/state-service.ts +1 -1
  347. package/src/modules/messages/api/route.ts +134 -123
  348. package/src/modules/messages/api/unread-count/route.ts +19 -16
  349. package/src/modules/messages/commands/confirmations.ts +1 -1
  350. package/src/modules/messages/commands/messages.ts +3 -3
  351. package/src/modules/messages/data/entities.ts +2 -1
  352. package/src/modules/messages/lib/email-sender.ts +1 -1
  353. package/src/modules/messages/lib/searchLookup.ts +16 -13
  354. package/src/modules/messages/lib/tokenConsumption.ts +16 -8
  355. package/src/modules/notifications/data/entities.ts +2 -1
  356. package/src/modules/notifications/lib/notificationRecipients.ts +42 -26
  357. package/src/modules/notifications/lib/notificationService.ts +53 -42
  358. package/src/modules/notifications/workers/create-notification.worker.ts +20 -17
  359. package/src/modules/payment_gateways/api/transactions/route.ts +2 -2
  360. package/src/modules/payment_gateways/data/entities.ts +2 -1
  361. package/src/modules/payment_gateways/lib/gateway-service.ts +1 -1
  362. package/src/modules/payment_gateways/lib/webhook-utils.ts +2 -2
  363. package/src/modules/perspectives/data/entities.ts +1 -1
  364. package/src/modules/planner/data/entities.ts +1 -1
  365. package/src/modules/progress/data/entities.ts +2 -1
  366. package/src/modules/progress/lib/progressServiceImpl.ts +1 -1
  367. package/src/modules/query_index/api/status.ts +85 -71
  368. package/src/modules/query_index/cli.ts +51 -31
  369. package/src/modules/query_index/data/entities.ts +1 -1
  370. package/src/modules/query_index/di.ts +41 -16
  371. package/src/modules/query_index/lib/batch.ts +68 -55
  372. package/src/modules/query_index/lib/coverage.ts +115 -88
  373. package/src/modules/query_index/lib/engine.ts +1036 -1096
  374. package/src/modules/query_index/lib/indexer.ts +115 -79
  375. package/src/modules/query_index/lib/jobs.ts +51 -31
  376. package/src/modules/query_index/lib/purge.ts +25 -19
  377. package/src/modules/query_index/lib/reindexer.ts +97 -84
  378. package/src/modules/query_index/lib/search-tokens.ts +67 -36
  379. package/src/modules/query_index/lib/stale.ts +14 -17
  380. package/src/modules/query_index/lib/subscriber-scope.ts +6 -5
  381. package/src/modules/query_index/subscribers/delete_one.ts +9 -6
  382. package/src/modules/resources/commands/tag-assignments.ts +1 -1
  383. package/src/modules/resources/commands/tags.ts +1 -1
  384. package/src/modules/resources/data/entities.ts +2 -1
  385. package/src/modules/sales/commands/documentAddresses.ts +2 -2
  386. package/src/modules/sales/commands/notes.ts +1 -1
  387. package/src/modules/sales/commands/tags.ts +1 -1
  388. package/src/modules/sales/data/enrichers.ts +17 -13
  389. package/src/modules/sales/data/entities.ts +2 -11
  390. package/src/modules/shipping_carriers/data/entities.ts +2 -1
  391. package/src/modules/shipping_carriers/lib/shipping-service.ts +1 -1
  392. package/src/modules/shipping_carriers/lib/webhook-utils.ts +2 -2
  393. package/src/modules/staff/data/entities.ts +1 -1
  394. package/src/modules/translations/api/[entityType]/[entityId]/route.ts +14 -11
  395. package/src/modules/translations/api/context.ts +4 -4
  396. package/src/modules/translations/commands/translations.ts +116 -81
  397. package/src/modules/translations/components/TranslationManager.tsx +23 -14
  398. package/src/modules/translations/data/entities.ts +1 -1
  399. package/src/modules/translations/i18n/de.json +1 -0
  400. package/src/modules/translations/i18n/en.json +1 -0
  401. package/src/modules/translations/i18n/es.json +1 -0
  402. package/src/modules/translations/i18n/pl.json +1 -0
  403. package/src/modules/translations/lib/apply.ts +6 -6
  404. package/src/modules/translations/lib/batch.ts +9 -7
  405. package/src/modules/translations/subscribers/cleanup.ts +10 -11
  406. package/src/modules/workflows/api/definitions/route.ts +1 -1
  407. package/src/modules/workflows/cli.ts +5 -5
  408. package/src/modules/workflows/data/entities.ts +2 -1
  409. package/src/modules/workflows/lib/event-logger.ts +2 -2
  410. package/src/modules/workflows/lib/seeds.ts +16 -1
  411. package/src/modules/workflows/lib/step-handler.ts +3 -3
  412. package/src/modules/workflows/lib/task-handler.ts +1 -1
  413. package/src/modules/workflows/lib/transition-handler.ts +1 -1
  414. package/src/modules/workflows/lib/workflow-executor.ts +2 -2
@@ -1,5 +1,6 @@
1
1
  import { SortDir } from "@open-mercato/shared/lib/query/types";
2
2
  import { resolveEntityTableName } from "@open-mercato/shared/lib/query/engine";
3
+ import { sql } from "kysely";
3
4
  import { readCoverageSnapshot, refreshCoverageSnapshot } from "./coverage.js";
4
5
  import { createProfiler, shouldEnableProfiler } from "@open-mercato/shared/lib/profiler";
5
6
  import { decryptIndexDocCustomFields } from "@open-mercato/shared/lib/encryption/indexDoc";
@@ -82,6 +83,11 @@ class HybridQueryEngine {
82
83
  return null;
83
84
  }
84
85
  }
86
+ getDb() {
87
+ const emAny = this.em;
88
+ if (typeof emAny.getKysely === "function") return emAny.getKysely();
89
+ throw new Error("HybridQueryEngine requires an EntityManager exposing getKysely() (MikroORM v7)");
90
+ }
85
91
  async query(entity, opts = {}) {
86
92
  const ext = opts.extensions;
87
93
  let hybridExtCtx = null;
@@ -148,8 +154,8 @@ class HybridQueryEngine {
148
154
  throw err;
149
155
  }
150
156
  }
151
- const knex = this.getKnex();
152
- profiler.mark("query:knex_ready");
157
+ const db = this.getDb();
158
+ profiler.mark("query:db_ready");
153
159
  const baseTable = resolveEntityTableName(this.em, entity);
154
160
  profiler.mark("query:base_table_resolved");
155
161
  const searchConfig = resolveSearchConfig();
@@ -261,10 +267,11 @@ class HybridQueryEngine {
261
267
  }
262
268
  }
263
269
  const qualify = (col) => `b.${col}`;
264
- let builder = knex({ b: baseTable });
265
- const hasCustomFieldFilters = cfFilters.length > 0;
266
- const canOptimizeCount = !hasCustomFieldFilters;
267
- let optimizedCountBuilder = canOptimizeCount ? knex({ b: baseTable }) : null;
270
+ const columns = await this.getBaseColumnsForEntity(entity);
271
+ const hasOrganizationColumn = await this.columnExists(baseTable, "organization_id");
272
+ const hasTenantColumn = await this.columnExists(baseTable, "tenant_id");
273
+ const hasDeletedColumn = await this.columnExists(baseTable, "deleted_at");
274
+ if (!opts.tenantId) throw new Error("QueryEngine: tenantId is required");
268
275
  const resolvedJoinsConfig = resolveJoins(
269
276
  baseTable,
270
277
  [...opts.joins ?? [], ...buildFilterableCustomFieldJoins(opts.customFieldSources)],
@@ -280,76 +287,22 @@ class HybridQueryEngine {
280
287
  aliasTables.set(join.alias, join.table);
281
288
  }
282
289
  const { baseFilters, joinFilters } = partitionFilters(baseTable, normalizedFilters, joinMap);
283
- if (!opts.tenantId) throw new Error("QueryEngine: tenantId is required");
284
- const hasOrganizationColumn = await this.columnExists(baseTable, "organization_id");
285
- const hasTenantColumn = await this.columnExists(baseTable, "tenant_id");
286
- const hasDeletedColumn = await this.columnExists(baseTable, "deleted_at");
287
290
  const searchRuntimeBase = {
288
291
  enabled: false,
289
292
  config: searchConfig,
290
293
  organizationScope: orgScope,
291
294
  tenantId: opts.tenantId ?? null
292
295
  };
293
- if (orgScope && hasOrganizationColumn) {
294
- builder = this.applyOrganizationScope(builder, qualify("organization_id"), orgScope);
295
- if (optimizedCountBuilder) optimizedCountBuilder = this.applyOrganizationScope(optimizedCountBuilder, qualify("organization_id"), orgScope);
296
- }
297
- if (hasTenantColumn) {
298
- builder = builder.where(qualify("tenant_id"), opts.tenantId);
299
- if (optimizedCountBuilder) optimizedCountBuilder = optimizedCountBuilder.where(qualify("tenant_id"), opts.tenantId);
300
- }
301
- if (!opts.withDeleted && hasDeletedColumn) {
302
- builder = builder.whereNull(qualify("deleted_at"));
303
- if (optimizedCountBuilder) optimizedCountBuilder = optimizedCountBuilder.whereNull(qualify("deleted_at"));
304
- }
305
- const baseJoinParts = [];
306
- baseJoinParts.push(`ei.entity_type = ${knex.raw("?", [entity]).toString()}`);
307
- baseJoinParts.push(`ei.entity_id = (${qualify("id")}::text)`);
308
- if (hasOrganizationColumn) {
309
- baseJoinParts.push(`ei.organization_id = ${qualify("organization_id")}`);
310
- baseJoinParts.push("ei.organization_id is not null");
311
- }
312
- if (hasTenantColumn) {
313
- baseJoinParts.push(`ei.tenant_id = ${qualify("tenant_id")}`);
314
- baseJoinParts.push("ei.tenant_id is not null");
315
- }
316
- if (!opts.withDeleted) baseJoinParts.push(`ei.deleted_at is null`);
317
- builder = builder.leftJoin({ ei: "entity_indexes" }, knex.raw(baseJoinParts.join(" AND ")));
318
- const columns = await this.getBaseColumnsForEntity(entity);
319
296
  const indexSources = [{ alias: "ei", entityId: entity, recordIdColumn: "b.id" }];
297
+ let preparedCfSources = [];
320
298
  const shouldAttachCustomSources = Array.isArray(opts.customFieldSources) && opts.customFieldSources.length > 0 && (wantsCf || searchEnabled);
321
299
  if (shouldAttachCustomSources) {
322
- const prepared = this.prepareCustomFieldSources(knex, builder, opts.customFieldSources ?? [], qualify);
323
- builder = prepared.builder;
324
- for (const source of prepared.sources) {
325
- const fragments = [];
326
- fragments.push(`${source.indexAlias}.entity_type = ${knex.raw("?", [source.entityId]).toString()}`);
327
- fragments.push(`${source.indexAlias}.entity_id = (${knex.raw("??::text", [`${source.alias}.${source.recordIdColumn}`]).toString()})`);
328
- const orgExpr = source.organizationField ? knex.raw("??", [`${source.alias}.${source.organizationField}`]).toString() : columns.has("organization_id") ? qualify("organization_id") : null;
329
- if (orgExpr) {
330
- fragments.push(`${source.indexAlias}.organization_id = ${orgExpr}`);
331
- fragments.push(`${source.indexAlias}.organization_id is not null`);
332
- }
333
- const tenantExpr = source.tenantField ? knex.raw("??", [`${source.alias}.${source.tenantField}`]).toString() : columns.has("tenant_id") ? qualify("tenant_id") : null;
334
- if (tenantExpr) {
335
- fragments.push(`${source.indexAlias}.tenant_id = ${tenantExpr}`);
336
- fragments.push(`${source.indexAlias}.tenant_id is not null`);
337
- }
338
- if (!opts.withDeleted) fragments.push(`${source.indexAlias}.deleted_at is null`);
339
- builder = builder.leftJoin({ [source.indexAlias]: "entity_indexes" }, knex.raw(fragments.join(" AND ")));
300
+ preparedCfSources = this.prepareCustomFieldSources(opts.customFieldSources ?? []);
301
+ for (const source of preparedCfSources) {
340
302
  indexSources.push({ alias: source.indexAlias, entityId: source.entityId, recordIdColumn: `${source.alias}.${source.recordIdColumn}` });
341
303
  }
342
304
  }
343
- if (debugEnabled) {
344
- this.debug("query:index-sources", {
345
- entity,
346
- sources: indexSources.map((src) => ({ alias: src.alias, entity: src.entityId }))
347
- });
348
- }
349
- const searchSources = indexSources.map((src) => ({
350
- entity: String(src.entityId),
351
- recordIdColumn: src.recordIdColumn
352
- })).filter((src) => src.recordIdColumn && src.entity);
305
+ const searchSources = indexSources.map((src) => ({ entity: String(src.entityId), recordIdColumn: src.recordIdColumn })).filter((src) => src.recordIdColumn && src.entity);
353
306
  const hasSearchTokens = searchEnabled && searchSources.length ? await this.searchSourcesHaveTokens(searchSources, opts.tenantId ?? null, orgScope) : false;
354
307
  const searchRuntime = { ...searchRuntimeBase, searchSources, enabled: searchEnabled && hasSearchTokens };
355
308
  const joinSearchAvailability = /* @__PURE__ */ new Map();
@@ -372,24 +325,18 @@ class HybridQueryEngine {
372
325
  blocklistedFields: searchConfig.blocklistedFields
373
326
  }
374
327
  });
375
- if (!searchEnabled) {
376
- this.logSearchDebug("search:disabled", { entity, baseTable });
377
- } else if (!hasSearchTokens) {
378
- this.logSearchDebug("search:no-search-tokens", {
379
- entity,
380
- baseTable,
381
- tenantId: opts.tenantId ?? null,
382
- organizationScope: orgScope,
383
- searchSources
384
- });
385
- }
328
+ if (!searchEnabled) this.logSearchDebug("search:disabled", { entity, baseTable });
329
+ else if (!hasSearchTokens) this.logSearchDebug("search:no-search-tokens", {
330
+ entity,
331
+ baseTable,
332
+ tenantId: opts.tenantId ?? null,
333
+ organizationScope: orgScope,
334
+ searchSources
335
+ });
386
336
  }
387
337
  const hasNonBaseSearchSource = searchSources.some(
388
338
  (src) => src.entity !== String(entity) || src.recordIdColumn !== "b.id"
389
339
  );
390
- if (hasNonBaseSearchSource) {
391
- optimizedCountBuilder = null;
392
- }
393
340
  if (!partialIndexWarning && Array.isArray(opts.customFieldSources) && opts.customFieldSources.length > 0 && this.isForcePartialIndexEnabled()) {
394
341
  const seen = /* @__PURE__ */ new Set([entity]);
395
342
  for (const source of opts.customFieldSources) {
@@ -444,14 +391,7 @@ class HybridQueryEngine {
444
391
  const globalGap = globalBase > 0 && globalIndexed < globalBase || globalIndexed > globalBase;
445
392
  if (globalGap) {
446
393
  console.warn("[HybridQueryEngine] Partial index coverage detected at global scope; forcing query index usage due to FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES:", { entity, baseCount: globalBase, indexedCount: globalIndexed, scope: "global" });
447
- if (debugEnabled) {
448
- this.debug("query:partial-coverage:forced", {
449
- entity,
450
- baseCount: globalBase,
451
- indexedCount: globalIndexed,
452
- scope: "global"
453
- });
454
- }
394
+ if (debugEnabled) this.debug("query:partial-coverage:forced", { entity, baseCount: globalBase, indexedCount: globalIndexed, scope: "global" });
455
395
  partialIndexWarning = {
456
396
  entity,
457
397
  entityLabel: this.resolveEntityLabel(entity),
@@ -462,12 +402,7 @@ class HybridQueryEngine {
462
402
  }
463
403
  }
464
404
  } catch (err) {
465
- if (debugEnabled) {
466
- this.debug("query:partial-coverage:global-check-failed", {
467
- entity,
468
- error: err instanceof Error ? err.message : err
469
- });
470
- }
405
+ if (debugEnabled) this.debug("query:partial-coverage:global-check-failed", { entity, error: err instanceof Error ? err.message : err });
471
406
  }
472
407
  }
473
408
  const resolveBaseColumn = (field) => {
@@ -475,38 +410,81 @@ class HybridQueryEngine {
475
410
  if (field === "organization_id" && columns.has("id")) return "id";
476
411
  return null;
477
412
  };
478
- for (const filter of cfFilters) {
479
- builder = this.applyCfFilterAcrossSources(
480
- knex,
481
- builder,
482
- filter.field,
483
- filter.op,
484
- filter.value,
485
- indexSources,
486
- searchRuntime
487
- );
488
- }
489
- const regularBaseFilters = baseFilters.filter((filter) => !filter.orGroup);
490
- const orGroupFilters = baseFilters.filter((filter) => filter.orGroup);
491
- for (const filter of regularBaseFilters) {
492
- const fieldName = String(filter.field);
493
- const baseField = resolveBaseColumn(fieldName);
494
- if (!baseField) {
495
- builder = this.applyIndexDocFilterFromAlias(
496
- knex,
497
- builder,
498
- "ei",
499
- entity,
500
- fieldName,
413
+ const applyBaseScope = (q) => {
414
+ let next = q;
415
+ if (orgScope && hasOrganizationColumn) {
416
+ next = this.applyOrganizationScope(next, qualify("organization_id"), orgScope);
417
+ }
418
+ if (hasTenantColumn) {
419
+ next = next.where(qualify("tenant_id"), "=", opts.tenantId);
420
+ }
421
+ if (!opts.withDeleted && hasDeletedColumn) {
422
+ next = next.where(qualify("deleted_at"), "is", null);
423
+ }
424
+ return next;
425
+ };
426
+ const applyEntityIndexesJoin = (q) => {
427
+ return q.leftJoin("entity_indexes as ei", (jb) => {
428
+ let jc = jb.on("ei.entity_type", "=", String(entity)).onRef("ei.entity_id", "=", sql`(${sql.ref(qualify("id"))}::text)`);
429
+ if (hasOrganizationColumn) {
430
+ jc = jc.onRef("ei.organization_id", "=", qualify("organization_id")).on("ei.organization_id", "is not", null);
431
+ }
432
+ if (hasTenantColumn) {
433
+ jc = jc.onRef("ei.tenant_id", "=", qualify("tenant_id")).on("ei.tenant_id", "is not", null);
434
+ }
435
+ if (!opts.withDeleted) {
436
+ jc = jc.on("ei.deleted_at", "is", null);
437
+ }
438
+ return jc;
439
+ });
440
+ };
441
+ const applyCustomFieldSourceJoins = (q) => {
442
+ let next = q;
443
+ for (const source of preparedCfSources) {
444
+ const join = (opts.customFieldSources ?? []).find((s) => s && (s.alias ?? void 0) === source.alias)?.join;
445
+ if (!join) continue;
446
+ const joinType = (join.type ?? "left") === "inner" ? "innerJoin" : "leftJoin";
447
+ next = next[joinType](`${source.table} as ${source.alias}`, (jb) => jb.onRef(`${source.alias}.${join.toField}`, "=", qualify(join.fromField)));
448
+ next = next.leftJoin(`entity_indexes as ${source.indexAlias}`, (jb) => {
449
+ let jc = jb.on(`${source.indexAlias}.entity_type`, "=", String(source.entityId)).onRef(`${source.indexAlias}.entity_id`, "=", sql`(${sql.ref(`${source.alias}.${source.recordIdColumn}`)}::text)`);
450
+ const orgRef = source.organizationField ? `${source.alias}.${source.organizationField}` : columns.has("organization_id") ? qualify("organization_id") : null;
451
+ if (orgRef) {
452
+ jc = jc.onRef(`${source.indexAlias}.organization_id`, "=", orgRef).on(`${source.indexAlias}.organization_id`, "is not", null);
453
+ }
454
+ const tenantRef = source.tenantField ? `${source.alias}.${source.tenantField}` : columns.has("tenant_id") ? qualify("tenant_id") : null;
455
+ if (tenantRef) {
456
+ jc = jc.onRef(`${source.indexAlias}.tenant_id`, "=", tenantRef).on(`${source.indexAlias}.tenant_id`, "is not", null);
457
+ }
458
+ if (!opts.withDeleted) jc = jc.on(`${source.indexAlias}.deleted_at`, "is", null);
459
+ return jc;
460
+ });
461
+ }
462
+ return next;
463
+ };
464
+ const applyCfFilters = (q) => {
465
+ let next = q;
466
+ for (const filter of cfFilters) {
467
+ next = this.applyCfFilterAcrossSources(
468
+ next,
469
+ filter.field,
501
470
  filter.op,
502
471
  filter.value,
503
- "b.id",
472
+ indexSources,
504
473
  searchRuntime
505
474
  );
506
- if (optimizedCountBuilder) {
507
- optimizedCountBuilder = this.applyIndexDocFilterFromAlias(
508
- knex,
509
- optimizedCountBuilder,
475
+ }
476
+ return next;
477
+ };
478
+ const regularBaseFilters = baseFilters.filter((filter) => !filter.orGroup);
479
+ const orGroupFilters = baseFilters.filter((filter) => filter.orGroup);
480
+ const applyRegularBaseFilters = (q) => {
481
+ let next = q;
482
+ for (const filter of regularBaseFilters) {
483
+ const fieldName = String(filter.field);
484
+ const baseField = resolveBaseColumn(fieldName);
485
+ if (!baseField) {
486
+ next = this.applyIndexDocFilterFromAlias(
487
+ next,
510
488
  "ei",
511
489
  entity,
512
490
  fieldName,
@@ -515,29 +493,20 @@ class HybridQueryEngine {
515
493
  "b.id",
516
494
  searchRuntime
517
495
  );
496
+ continue;
518
497
  }
519
- continue;
520
- }
521
- const column = qualify(baseField);
522
- builder = this.applyColumnFilter(builder, column, filter, {
523
- ...searchRuntime,
524
- knex,
525
- entity,
526
- field: fieldName,
527
- recordIdColumn: "b.id"
528
- });
529
- if (optimizedCountBuilder) {
530
- optimizedCountBuilder = this.applyColumnFilter(optimizedCountBuilder, column, filter, {
498
+ const column = qualify(baseField);
499
+ next = this.applyColumnFilter(next, column, filter, {
531
500
  ...searchRuntime,
532
- knex,
533
501
  entity,
534
502
  field: fieldName,
535
503
  recordIdColumn: "b.id"
536
504
  });
537
505
  }
538
- }
539
- const applyOrGroupedBaseFilters = (target) => {
540
- if (!target || orGroupFilters.length === 0) return target;
506
+ return next;
507
+ };
508
+ const applyOrGroupedBaseFilters = (q) => {
509
+ if (orGroupFilters.length === 0) return q;
541
510
  const groups = /* @__PURE__ */ new Map();
542
511
  for (const filter of orGroupFilters) {
543
512
  if (!filter.orGroup) continue;
@@ -545,99 +514,61 @@ class HybridQueryEngine {
545
514
  existing.push(filter);
546
515
  groups.set(filter.orGroup, existing);
547
516
  }
548
- let next = target;
517
+ let next = q;
549
518
  for (const [, groupFilters] of groups) {
550
519
  if (!groupFilters.length) continue;
551
- next = next.where((groupBuilder) => {
552
- groupFilters.forEach((filter, index) => {
553
- const fieldName = String(filter.field);
554
- const baseField = resolveBaseColumn(fieldName);
555
- const applyCondition = (conditionBuilder) => {
556
- if (!baseField) {
557
- this.applyIndexDocFilterFromAlias(
558
- knex,
559
- conditionBuilder,
560
- "ei",
561
- entity,
562
- fieldName,
563
- filter.op,
564
- filter.value,
565
- "b.id",
566
- searchRuntime
567
- );
568
- return;
569
- }
570
- this.applyColumnFilter(conditionBuilder, qualify(baseField), filter, {
571
- ...searchRuntime,
572
- knex,
573
- entity,
574
- field: fieldName,
575
- recordIdColumn: "b.id"
576
- });
577
- };
578
- if (index === 0) {
579
- applyCondition(groupBuilder);
580
- return;
581
- }
582
- groupBuilder.orWhere((conditionBuilder) => {
583
- applyCondition(conditionBuilder);
584
- });
585
- });
586
- });
520
+ next = next.where((eb) => eb.or(
521
+ groupFilters.map((filter) => this.buildBaseFilterExpression(eb, filter, resolveBaseColumn, qualify, entity, searchRuntime))
522
+ ));
587
523
  }
588
524
  return next;
589
525
  };
590
- builder = applyOrGroupedBaseFilters(builder) ?? builder;
591
- optimizedCountBuilder = applyOrGroupedBaseFilters(optimizedCountBuilder);
592
526
  const applyAliasScopes = async (target, aliasName) => {
527
+ let next = target;
593
528
  const tableName = aliasTables.get(aliasName);
594
- if (!tableName) return;
529
+ if (!tableName) return next;
595
530
  if (orgScope && await this.columnExists(tableName, "organization_id")) {
596
- this.applyOrganizationScope(target, `${aliasName}.organization_id`, orgScope);
531
+ next = this.applyOrganizationScope(next, `${aliasName}.organization_id`, orgScope);
597
532
  }
598
533
  if (opts.tenantId && await this.columnExists(tableName, "tenant_id")) {
599
- target.where(`${aliasName}.tenant_id`, opts.tenantId);
534
+ next = next.where(`${aliasName}.tenant_id`, "=", opts.tenantId);
600
535
  }
601
536
  if (!opts.withDeleted && await this.columnExists(tableName, "deleted_at")) {
602
- target.whereNull(`${aliasName}.deleted_at`);
537
+ next = next.where(`${aliasName}.deleted_at`, "is", null);
603
538
  }
539
+ return next;
604
540
  };
605
- const applyJoinFilterOp = (target, column, op, value) => {
541
+ const applyJoinFilterOpFn = (target, column, op, value) => {
606
542
  switch (op) {
607
543
  case "eq":
608
- target.where(column, value);
609
- break;
544
+ return target.where(column, "=", value);
610
545
  case "ne":
611
- target.whereNot(column, value);
612
- break;
546
+ return target.where(column, "!=", value);
613
547
  case "gt":
548
+ return target.where(column, ">", value);
614
549
  case "gte":
550
+ return target.where(column, ">=", value);
615
551
  case "lt":
616
- case "lte": {
617
- const operator = op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=";
618
- target.where(column, operator, value);
619
- break;
620
- }
552
+ return target.where(column, "<", value);
553
+ case "lte":
554
+ return target.where(column, "<=", value);
621
555
  case "in":
622
- target.whereIn(column, this.toArray(value));
623
- break;
556
+ return target.where(column, "in", this.toArray(value));
624
557
  case "nin":
625
- target.whereNotIn(column, this.toArray(value));
626
- break;
558
+ return target.where(column, "not in", this.toArray(value));
627
559
  case "like":
628
- target.where(column, "like", value);
629
- break;
560
+ return target.where(column, "like", value);
630
561
  case "ilike":
631
- target.where(column, "ilike", value);
632
- break;
562
+ return target.where(column, "ilike", value);
633
563
  case "exists":
634
- value ? target.whereNotNull(column) : target.whereNull(column);
635
- break;
564
+ return value ? target.where(column, "is not", null) : target.where(column, "is", null);
565
+ default:
566
+ return target;
636
567
  }
637
568
  };
638
569
  const applyJoinSearchFilterOp = async (target, filter, _qualified, join) => {
639
570
  if (!searchEnabled || !join.entityId) return false;
640
- if (!["eq", "like", "ilike"].includes(filter.op)) return false;
571
+ if (!["like", "ilike"].includes(filter.op)) return false;
641
572
  if (typeof filter.value !== "string" || filter.value.trim().length === 0) return false;
642
573
  let searchAvailable = joinSearchAvailability.get(join.entityId);
643
574
  if (searchAvailable === void 0) {
@@ -648,7 +579,6 @@ class HybridQueryEngine {
648
579
  const tokens = tokenizeText(String(filter.value), searchConfig);
649
580
  if (!tokens.hashes.length) return false;
650
581
  return this.applySearchTokens(target, {
651
- knex,
652
582
  entity: String(join.entityId),
653
583
  field: filter.column,
654
584
  hashes: tokens.hashes,
@@ -657,43 +587,40 @@ class HybridQueryEngine {
657
587
  organizationScope: orgScope
658
588
  });
659
589
  };
660
- await applyJoinFilters({
661
- knex,
662
- baseTable,
663
- builder,
664
- joinMap,
665
- joinFilters,
666
- aliasTables,
667
- qualifyBase: (column) => qualify(column),
668
- applyAliasScope: (target, alias) => applyAliasScopes(target, alias),
669
- applyFilterOp: (target, column, op, value) => applyJoinFilterOp(target, column, op, value),
670
- applyJoinFilterOp: (target, filter, qualified, join) => applyJoinSearchFilterOp(target, filter, qualified, join),
671
- columnExists: (tbl, column) => this.columnExists(tbl, column)
672
- });
673
- if (optimizedCountBuilder) {
674
- await applyJoinFilters({
675
- knex,
590
+ const applyQueryShape = async (q) => {
591
+ let next = applyBaseScope(q);
592
+ next = applyEntityIndexesJoin(next);
593
+ next = applyCustomFieldSourceJoins(next);
594
+ next = applyCfFilters(next);
595
+ next = applyRegularBaseFilters(next);
596
+ next = applyOrGroupedBaseFilters(next);
597
+ next = await applyJoinFilters({
598
+ db,
676
599
  baseTable,
677
- builder: optimizedCountBuilder,
600
+ builder: next,
678
601
  joinMap,
679
602
  joinFilters,
680
603
  aliasTables,
681
604
  qualifyBase: (column) => qualify(column),
682
- applyAliasScope: (target, alias) => applyAliasScopes(target, alias),
683
- applyFilterOp: (target, column, op, value) => applyJoinFilterOp(target, column, op, value),
684
- applyJoinFilterOp: (target, filter, qualified, join) => applyJoinSearchFilterOp(target, filter, qualified, join),
605
+ applyAliasScope: async (target, alias) => applyAliasScopes(target, alias),
606
+ applyFilterOp: (target, column, op, value) => applyJoinFilterOpFn(target, column, op, value),
607
+ applyJoinFilterOp: async (target, filter, qualified, join) => {
608
+ const applied = await applyJoinSearchFilterOp(target, filter, qualified, join);
609
+ return { applied, builder: target };
610
+ },
685
611
  columnExists: (tbl, column) => this.columnExists(tbl, column)
686
612
  });
687
- }
613
+ return next;
614
+ };
615
+ const hasCustomFieldFilters = cfFilters.length > 0;
616
+ const canOptimizeCount = !hasCustomFieldFilters && !hasNonBaseSearchSource;
688
617
  const selectFieldSet = new Set(opts.fields && opts.fields.length ? opts.fields.map(String) : Array.from(columns.keys()));
689
618
  if (opts.includeCustomFields === true) {
690
619
  const entityIds = Array.from(new Set(indexSources.map((src) => String(src.entityId))));
691
620
  try {
692
621
  const resolvedKeys = await this.resolveAvailableCustomFieldKeys(entityIds, opts.tenantId ?? null);
693
622
  resolvedKeys.forEach((key) => selectFieldSet.add(`cf:${key}`));
694
- if (this.isDebugVerbosity()) {
695
- this.debug("query:cf:resolved-keys", { entity, keys: resolvedKeys });
696
- }
623
+ if (this.isDebugVerbosity()) this.debug("query:cf:resolved-keys", { entity, keys: resolvedKeys });
697
624
  } catch (err) {
698
625
  console.warn("[HybridQueryEngine] Failed to resolve custom field keys for", entity, err);
699
626
  }
@@ -701,74 +628,107 @@ class HybridQueryEngine {
701
628
  opts.includeCustomFields.map((key) => String(key)).forEach((key) => selectFieldSet.add(`cf:${key}`));
702
629
  }
703
630
  const selectFields = Array.from(selectFieldSet);
704
- for (const field of selectFields) {
705
- const fieldName = String(field);
706
- if (fieldName.startsWith("cf:")) {
707
- const alias = this.sanitize(fieldName);
708
- const { jsonSql } = this.buildCfExpressions(knex, fieldName, indexSources);
709
- const exprSql = jsonSql === "NULL" ? "NULL::jsonb" : jsonSql;
710
- builder = builder.select(knex.raw(`${exprSql} as ??`, [alias]));
711
- } else if (columns.has(fieldName)) {
712
- builder = builder.select(knex.raw("?? as ??", [qualify(fieldName), fieldName]));
631
+ const applySelection = (q) => {
632
+ let next = q;
633
+ for (const field of selectFields) {
634
+ const fieldName = String(field);
635
+ if (fieldName.startsWith("cf:")) {
636
+ const alias = this.sanitize(fieldName);
637
+ const jsonExpr = this.buildCfJsonExprSql(fieldName, indexSources);
638
+ const exprRaw = jsonExpr ?? sql`NULL::jsonb`;
639
+ next = next.select(exprRaw.as(alias));
640
+ } else if (columns.has(fieldName)) {
641
+ next = next.select(`${qualify(fieldName)} as ${fieldName}`);
642
+ }
713
643
  }
714
- }
715
- for (const sort of opts.sort || []) {
716
- const fieldName = String(sort.field);
717
- if (fieldName.startsWith("cf:")) {
718
- const { textSql } = this.buildCfExpressions(knex, fieldName, indexSources);
719
- if (textSql !== "NULL") {
720
- const direction = sort.dir ?? SortDir.Asc;
721
- builder = builder.orderByRaw(`${textSql} ${direction}`);
644
+ return next;
645
+ };
646
+ const applySort = (q) => {
647
+ let next = q;
648
+ for (const s of opts.sort || []) {
649
+ const fieldName = String(s.field);
650
+ if (fieldName.startsWith("cf:")) {
651
+ const textExpr = this.buildCfTextExprSql(fieldName, indexSources);
652
+ if (textExpr) {
653
+ const direction = sql.raw(String(s.dir ?? SortDir.Asc));
654
+ next = next.orderBy(sql`${textExpr} ${direction}`);
655
+ }
656
+ } else {
657
+ const baseField = resolveBaseColumn(fieldName);
658
+ if (!baseField) continue;
659
+ next = next.orderBy(qualify(baseField), s.dir ?? SortDir.Asc);
722
660
  }
723
- } else {
724
- const baseField = resolveBaseColumn(fieldName);
725
- if (!baseField) continue;
726
- builder = builder.orderBy(qualify(baseField), sort.dir ?? SortDir.Asc);
727
661
  }
728
- }
662
+ return next;
663
+ };
729
664
  const page = opts.page?.page ?? 1;
730
665
  const pageSize = opts.page?.pageSize ?? 20;
731
666
  const sqlDebugEnabled = this.isSqlDebugEnabled();
732
667
  let total;
733
- if (optimizedCountBuilder) {
734
- const countSource = optimizedCountBuilder.clone().clearSelect().clearOrder().select(knex.raw(`${qualify("id")} as id`)).groupBy(qualify("id"));
735
- const countQuery = knex.from(countSource.as("sq")).count({ count: knex.raw("*") });
668
+ if (canOptimizeCount) {
669
+ const optimizedRoot = db.selectFrom(`${baseTable} as b`);
670
+ let countCore = applyBaseScope(optimizedRoot);
671
+ countCore = applyRegularBaseFilters(countCore);
672
+ countCore = applyOrGroupedBaseFilters(countCore);
673
+ countCore = await applyJoinFilters({
674
+ db,
675
+ baseTable,
676
+ builder: countCore,
677
+ joinMap,
678
+ joinFilters,
679
+ aliasTables,
680
+ qualifyBase: (column) => qualify(column),
681
+ applyAliasScope: async (target, alias) => applyAliasScopes(target, alias),
682
+ applyFilterOp: (target, column, op, value) => applyJoinFilterOpFn(target, column, op, value),
683
+ applyJoinFilterOp: async (target, filter, qualified, join) => {
684
+ const applied = await applyJoinSearchFilterOp(target, filter, qualified, join);
685
+ return { applied, builder: target };
686
+ },
687
+ columnExists: (tbl, column) => this.columnExists(tbl, column)
688
+ });
689
+ const sub = countCore.select(sql.ref(qualify("id")).as("id")).groupBy(qualify("id")).as("sq");
690
+ const countQuery = db.selectFrom(sub).select(sql`count(*)`.as("count"));
736
691
  if (debugEnabled && sqlDebugEnabled) {
737
- const { sql, bindings } = countQuery.clone().toSQL();
738
- this.debug("query:sql:count", { entity, sql, bindings });
692
+ const compiled = countQuery.compile();
693
+ this.debug("query:sql:count", { entity, sql: compiled.sql, bindings: compiled.parameters });
739
694
  }
740
695
  const countRow = await this.captureSqlTiming(
741
696
  "query:sql:count",
742
697
  entity,
743
- () => countQuery.first(),
698
+ () => countQuery.executeTakeFirst(),
744
699
  { optimized: true },
745
700
  profiler
746
701
  );
747
702
  total = this.parseCount(countRow);
748
703
  } else {
749
- const countBuilder = builder.clone().clearSelect().clearOrder().countDistinct(`${qualify("id")} as count`);
704
+ const countRoot = db.selectFrom(`${baseTable} as b`);
705
+ const countBuilder = (await applyQueryShape(countRoot)).select(sql`count(distinct ${sql.ref(qualify("id"))})`.as("count"));
750
706
  if (debugEnabled && sqlDebugEnabled) {
751
- const { sql, bindings } = countBuilder.clone().toSQL();
752
- this.debug("query:sql:count", { entity, sql, bindings });
707
+ const compiled = countBuilder.compile();
708
+ this.debug("query:sql:count", { entity, sql: compiled.sql, bindings: compiled.parameters });
753
709
  }
754
710
  const countRow = await this.captureSqlTiming(
755
711
  "query:sql:count",
756
712
  entity,
757
- () => countBuilder.first(),
713
+ () => countBuilder.executeTakeFirst(),
758
714
  { optimized: false },
759
715
  profiler
760
716
  );
761
717
  total = this.parseCount(countRow);
762
718
  }
763
- const dataBuilder = builder.clone().limit(pageSize).offset((page - 1) * pageSize);
719
+ const dataRoot = db.selectFrom(`${baseTable} as b`);
720
+ let dataBuilder = await applyQueryShape(dataRoot);
721
+ dataBuilder = applySelection(dataBuilder);
722
+ dataBuilder = applySort(dataBuilder);
723
+ dataBuilder = dataBuilder.limit(pageSize).offset((page - 1) * pageSize);
764
724
  if (debugEnabled && sqlDebugEnabled) {
765
- const { sql, bindings } = dataBuilder.clone().toSQL();
766
- this.debug("query:sql:data", { entity, sql, bindings, page, pageSize });
725
+ const compiled = dataBuilder.compile();
726
+ this.debug("query:sql:data", { entity, sql: compiled.sql, bindings: compiled.parameters, page, pageSize });
767
727
  }
768
728
  const itemsRaw = await this.captureSqlTiming(
769
729
  "query:sql:data",
770
730
  entity,
771
- () => dataBuilder,
731
+ () => dataBuilder.execute(),
772
732
  { page, pageSize },
773
733
  profiler
774
734
  );
@@ -816,9 +776,7 @@ class HybridQueryEngine {
816
776
  }
817
777
  const typedItems = items;
818
778
  let result = { items: typedItems, page, pageSize, total };
819
- if (partialIndexWarning) {
820
- result.meta = { partialIndexWarning };
821
- }
779
+ if (partialIndexWarning) result.meta = { partialIndexWarning };
822
780
  result = await applyAfterExtensions(result);
823
781
  finishProfile({
824
782
  result: "ok",
@@ -834,30 +792,15 @@ class HybridQueryEngine {
834
792
  throw err;
835
793
  }
836
794
  }
837
- getKnex() {
838
- const connection = this.em.getConnection();
839
- const withKnex = connection;
840
- if (typeof withKnex.getKnex === "function") {
841
- return withKnex.getKnex();
842
- }
843
- throw new Error("HybridQueryEngine requires a SQL connection that exposes getKnex()");
844
- }
845
- prepareCustomFieldSources(knex, builder, sources, qualify) {
846
- let current = builder;
795
+ prepareCustomFieldSources(sources) {
847
796
  const prepared = [];
848
797
  sources.forEach((source, index) => {
849
798
  if (!source) return;
850
799
  const joinTable = source.table ?? resolveEntityTableName(this.em, source.entityId);
851
800
  const alias = source.alias ?? `cfs_${index}`;
852
- const join = source.join;
853
- if (!join) {
801
+ if (!source.join) {
854
802
  throw new Error(`QueryEngine: customFieldSources entry for ${String(source.entityId)} requires a join configuration`);
855
803
  }
856
- const joinArgs = { [alias]: joinTable };
857
- const joinCallback = function() {
858
- this.on(`${alias}.${join.toField}`, "=", qualify(join.fromField));
859
- };
860
- current = (join.type ?? "left") === "inner" ? current.join(joinArgs, joinCallback) : current.leftJoin(joinArgs, joinCallback);
861
804
  prepared.push({
862
805
  alias,
863
806
  indexAlias: `ei_${alias}`,
@@ -868,17 +811,26 @@ class HybridQueryEngine {
868
811
  table: joinTable
869
812
  });
870
813
  });
871
- return { builder: current, sources: prepared };
814
+ return prepared;
872
815
  }
873
816
  async isCustomEntity(entity) {
874
817
  try {
875
- const knex = this.getKnex();
876
- const row = await knex("custom_entities").where({ entity_id: entity, is_active: true }).first();
818
+ const db = this.getDb();
819
+ const row = await db.selectFrom("custom_entities").select("id").where("entity_id", "=", entity).where("is_active", "=", true).executeTakeFirst();
877
820
  return !!row;
878
821
  } catch {
879
822
  return false;
880
823
  }
881
824
  }
825
+ /**
826
+ * Adds a WHERE EXISTS / OR WHERE EXISTS subquery that matches
827
+ * `search_tokens` for the supplied (entity, field) against the
828
+ * provided record id column.
829
+ *
830
+ * Returns true when the sub-query was applied (i.e. tokens were
831
+ * non-empty). Caller is responsible for the calling context
832
+ * (direct where vs. inside `eb.or([...])`).
833
+ */
882
834
  applySearchTokens(q, opts) {
883
835
  if (!opts.hashes.length) {
884
836
  this.logSearchDebug("search:skip-no-hashes", {
@@ -890,8 +842,6 @@ class HybridQueryEngine {
890
842
  return false;
891
843
  }
892
844
  const alias = `st_${this.searchAliasSeq++}`;
893
- const combineWith = opts.combineWith === "or" ? "orWhereExists" : "whereExists";
894
- const engine = this;
895
845
  this.logSearchDebug("search:apply-search-tokens", {
896
846
  entity: opts.entity,
897
847
  field: opts.field,
@@ -901,63 +851,67 @@ class HybridQueryEngine {
901
851
  organizationScope: opts.organizationScope,
902
852
  combineWith: opts.combineWith ?? "and"
903
853
  });
904
- q[combineWith](function() {
905
- this.select(1).from({ [alias]: "search_tokens" }).where(`${alias}.entity_type`, opts.entity).andWhere(`${alias}.field`, opts.field).andWhereRaw("?? = ??::text", [`${alias}.entity_id`, opts.recordIdColumn]).whereIn(`${alias}.token_hash`, opts.hashes).groupBy(`${alias}.entity_id`, `${alias}.field`).havingRaw(`count(distinct ${alias}.token_hash) >= ?`, [opts.hashes.length]);
854
+ const engine = this;
855
+ const buildSub = (eb) => {
856
+ let sub = eb.selectFrom(`search_tokens as ${alias}`).select(sql`1`.as("one")).where(`${alias}.entity_type`, "=", opts.entity).where(`${alias}.field`, "=", opts.field).where(sql`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`).where(`${alias}.token_hash`, "in", opts.hashes).groupBy([`${alias}.entity_id`, `${alias}.field`]).having(sql`count(distinct ${sql.ref(`${alias}.token_hash`)}) >= ${opts.hashes.length}`);
906
857
  if (opts.tenantId !== void 0) {
907
- this.andWhereRaw(`${alias}.tenant_id is not distinct from ?`, [opts.tenantId ?? null]);
858
+ sub = sub.where(sql`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`);
908
859
  }
909
860
  if (opts.organizationScope) {
910
- engine.applyOrganizationScope(this, `${alias}.organization_id`, opts.organizationScope);
861
+ sub = engine.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope);
911
862
  }
912
- });
863
+ return sub;
864
+ };
865
+ if (opts.combineWith === "or") {
866
+ ;
867
+ q.__pendingOrExists = buildSub(q);
868
+ return true;
869
+ }
870
+ ;
871
+ q.__applied = true;
872
+ const built = buildSub(q);
873
+ if (typeof q.where === "function") {
874
+ ;
875
+ q = q.where((eb) => eb.exists(built));
876
+ }
913
877
  return true;
914
878
  }
915
- jsonbRawAlias(knex, alias, key) {
879
+ /** SQL fragment for `cf:<key>` (or legacy bare key) as JSON across a single alias. */
880
+ jsonbSqlAlias(alias, key) {
916
881
  if (key.startsWith("cf:")) {
917
882
  const bare = key.slice(3);
918
- return knex.raw(`coalesce(${alias}.doc -> ?, ${alias}.doc -> ?)`, [key, bare]);
883
+ return sql`coalesce(${sql.ref(alias + ".doc")} -> ${key}, ${sql.ref(alias + ".doc")} -> ${bare})`;
919
884
  }
920
- return knex.raw(`${alias}.doc -> ?`, [key]);
885
+ return sql`${sql.ref(alias + ".doc")} -> ${key}`;
921
886
  }
922
- cfTextExprAlias(knex, alias, key) {
887
+ /** SQL fragment for `cf:<key>` (or legacy bare key) as text across a single alias. */
888
+ cfTextExprAlias(alias, key) {
923
889
  if (key.startsWith("cf:")) {
924
890
  const bare = key.slice(3);
925
- return knex.raw(`coalesce((${alias}.doc ->> ?), (${alias}.doc ->> ?))`, [key, bare]);
891
+ return sql`coalesce((${sql.ref(alias + ".doc")} ->> ${key}), (${sql.ref(alias + ".doc")} ->> ${bare}))`;
926
892
  }
927
- return knex.raw(`(${alias}.doc ->> ?)`, [key]);
893
+ return sql`(${sql.ref(alias + ".doc")} ->> ${key})`;
894
+ }
895
+ /** Build JSON/text SQL expressions across multiple index alias sources (coalesce over them). */
896
+ buildCfJsonExprSql(key, sources) {
897
+ if (!sources.length) return null;
898
+ const parts = sources.map((src) => this.jsonbSqlAlias(src.alias, key));
899
+ if (parts.length === 1) return parts[0];
900
+ return sql`coalesce(${sql.join(parts, sql`, `)})`;
928
901
  }
929
- buildCfExpressions(knex, key, sources) {
930
- if (!sources.length) return { jsonSql: "NULL", textSql: "NULL" };
931
- const jsonFragments = sources.map((source) => this.jsonbRawAlias(knex, source.alias, key).toString());
932
- const textFragments = sources.map((source) => this.cfTextExprAlias(knex, source.alias, key).toString());
933
- const jsonSql = jsonFragments.length === 1 ? jsonFragments[0] : `coalesce(${jsonFragments.join(", ")})`;
934
- const textSql = textFragments.length === 1 ? textFragments[0] : `coalesce(${textFragments.join(", ")})`;
935
- return { jsonSql, textSql };
902
+ buildCfTextExprSql(key, sources) {
903
+ if (!sources.length) return null;
904
+ const parts = sources.map((src) => this.cfTextExprAlias(src.alias, key));
905
+ if (parts.length === 1) return parts[0];
906
+ return sql`coalesce(${sql.join(parts, sql`, `)})`;
936
907
  }
937
- applyCfFilterAcrossSources(knex, builder, key, op, value, sources, search) {
908
+ applyCfFilterAcrossSources(builder, key, op, value, sources, search) {
938
909
  if (!sources.length) return builder;
939
910
  if ((op === "like" || op === "ilike") && search?.enabled && typeof value === "string") {
940
911
  const tokens = tokenizeText(String(value), search.config);
941
912
  const hashes = tokens.hashes;
942
913
  if (hashes.length) {
943
- let applied = false;
944
- if (sources.length) {
945
- builder = builder.where((qb) => {
946
- sources.forEach((source, idx) => {
947
- const ok = this.applySearchTokens(qb, {
948
- knex,
949
- entity: source.entityId,
950
- field: key,
951
- hashes,
952
- recordIdColumn: `${source.alias}.entity_id`,
953
- tenantId: search.tenantId ?? null,
954
- organizationScope: search.organizationScope ?? null,
955
- combineWith: idx === 0 ? "and" : "or"
956
- });
957
- if (ok) applied = true;
958
- });
959
- });
960
- }
914
+ const applied = this.applyMultiSourceSearchExists(builder, sources, key, hashes, search);
961
915
  this.logSearchDebug("search:cf-filter-across", {
962
916
  entity: sources.map((src) => src.entityId),
963
917
  field: key,
@@ -967,7 +921,7 @@ class HybridQueryEngine {
967
921
  tenantId: search.tenantId ?? null,
968
922
  organizationScope: search.organizationScope
969
923
  });
970
- if (applied) return builder;
924
+ if (applied.builder !== builder) return applied.builder;
971
925
  } else {
972
926
  this.logSearchDebug("search:cf-skip-empty-hashes", {
973
927
  entity: sources.map((src) => src.entityId),
@@ -977,193 +931,282 @@ class HybridQueryEngine {
977
931
  }
978
932
  return builder;
979
933
  }
980
- const { jsonSql, textSql } = this.buildCfExpressions(knex, key, sources);
981
- if (jsonSql === "NULL" || textSql === "NULL") return builder;
982
- const textExpr = knex.raw(textSql);
983
- const arrContains = (val) => knex.raw(`${jsonSql} @> ?::jsonb`, [JSON.stringify([val])]);
934
+ const textExpr = this.buildCfTextExprSql(key, sources);
935
+ const jsonExpr = this.buildCfJsonExprSql(key, sources);
936
+ if (!textExpr || !jsonExpr) return builder;
937
+ const arrContains = (val) => sql`${jsonExpr} @> ${JSON.stringify([val])}::jsonb`;
984
938
  switch (op) {
985
939
  case "eq":
986
- return builder.where((qb) => {
987
- qb.orWhere(textExpr, "=", value);
988
- qb.orWhere(arrContains(value));
989
- });
940
+ return builder.where((eb) => eb.or([
941
+ sql`${textExpr} = ${value}`,
942
+ arrContains(value)
943
+ ]));
990
944
  case "ne":
991
- return builder.whereNot(textExpr, "=", value);
945
+ return builder.where(sql`${textExpr} <> ${value}`);
992
946
  case "in": {
993
947
  const values = this.toArray(value);
994
- return builder.where((qb) => {
995
- values.forEach((val) => {
996
- qb.orWhere(textExpr, "=", val);
997
- qb.orWhere(arrContains(val));
998
- });
999
- });
948
+ return builder.where((eb) => eb.or(
949
+ values.flatMap((val) => [
950
+ sql`${textExpr} = ${val}`,
951
+ arrContains(val)
952
+ ])
953
+ ));
1000
954
  }
1001
955
  case "nin": {
1002
956
  const values = this.toArray(value);
1003
- return builder.whereNotIn(textExpr, values);
957
+ return builder.where(sql`${textExpr} not in (${sql.join(values.map((v) => sql`${v}`), sql`, `)})`);
1004
958
  }
1005
959
  case "like":
1006
- return builder.where(textExpr, "like", value);
960
+ return builder.where(sql`${textExpr} like ${value}`);
1007
961
  case "ilike":
1008
- return builder.where(textExpr, "ilike", value);
962
+ return builder.where(sql`${textExpr} ilike ${value}`);
1009
963
  case "exists":
1010
- return value ? builder.whereRaw(`${textExpr.toString()} is not null`) : builder.whereRaw(`${textExpr.toString()} is null`);
964
+ return value ? builder.where(sql`${textExpr} is not null`) : builder.where(sql`${textExpr} is null`);
1011
965
  case "gt":
1012
966
  case "gte":
1013
967
  case "lt":
1014
968
  case "lte": {
1015
- const operator = op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=";
1016
- return builder.where(textExpr, operator, value);
969
+ const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
970
+ return builder.where(sql`${textExpr} ${operator} ${value}`);
1017
971
  }
1018
972
  default:
1019
973
  return builder;
1020
974
  }
1021
975
  }
1022
- applyCfFilterFromAlias(knex, q, alias, entityType, key, op, value, search) {
1023
- const text = this.cfTextExprAlias(knex, alias, key);
1024
- const arrExpr = knex.raw(`(${alias}.doc -> ?)`, [key]);
1025
- const arrContains = (val) => knex.raw(`${arrExpr.toString()} @> ?::jsonb`, [JSON.stringify([val])]);
976
+ /** Apply a search-token EXISTS subquery across multiple sources (OR-joined). */
977
+ applyMultiSourceSearchExists(builder, sources, key, hashes, search) {
978
+ if (!sources.length || !hashes.length) return { builder, applied: false };
979
+ const next = builder.where((eb) => eb.or(
980
+ sources.map(
981
+ (source) => eb.exists(this.buildSearchTokensSub(eb, {
982
+ entity: String(source.entityId),
983
+ field: key,
984
+ hashes,
985
+ recordIdColumn: `${source.alias}.entity_id`,
986
+ tenantId: search.tenantId ?? null,
987
+ organizationScope: search.organizationScope ?? null
988
+ }))
989
+ )
990
+ ));
991
+ return { builder: next, applied: true };
992
+ }
993
+ /** Construct a search-token EXISTS subquery using the given ExpressionBuilder. */
994
+ buildSearchTokensSub(eb, opts) {
995
+ const alias = `st_${this.searchAliasSeq++}`;
996
+ let sub = eb.selectFrom(`search_tokens as ${alias}`).select(sql`1`.as("one")).where(`${alias}.entity_type`, "=", opts.entity).where(`${alias}.field`, "=", opts.field).where(sql`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`).where(`${alias}.token_hash`, "in", opts.hashes).groupBy([`${alias}.entity_id`, `${alias}.field`]).having(sql`count(distinct ${sql.ref(`${alias}.token_hash`)}) >= ${opts.hashes.length}`);
997
+ if (opts.tenantId !== void 0) {
998
+ sub = sub.where(sql`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`);
999
+ }
1000
+ if (opts.organizationScope) {
1001
+ sub = this.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope);
1002
+ }
1003
+ return sub;
1004
+ }
1005
+ applyCfFilterFromAlias(q, alias, entityType, key, op, value, search) {
1006
+ const textExpr = this.cfTextExprAlias(alias, key);
1007
+ const arrExpr = sql`(${sql.ref(alias + ".doc")} -> ${key})`;
1008
+ const arrContains = (val) => sql`${arrExpr} @> ${JSON.stringify([val])}::jsonb`;
1026
1009
  if ((op === "like" || op === "ilike") && search?.enabled && typeof value === "string") {
1027
1010
  const tokens = tokenizeText(String(value), search.config);
1028
1011
  const hashes = tokens.hashes;
1029
1012
  if (hashes.length) {
1030
- const applied = this.applySearchTokens(q, {
1031
- knex,
1013
+ const applied = q.where((eb) => eb.exists(this.buildSearchTokensSub(eb, {
1032
1014
  entity: entityType,
1033
1015
  field: key,
1034
1016
  hashes,
1035
1017
  recordIdColumn: `${alias}.entity_id`,
1036
1018
  tenantId: search.tenantId ?? null,
1037
1019
  organizationScope: search.organizationScope ?? null
1038
- });
1020
+ })));
1039
1021
  this.logSearchDebug("search:cf-filter", {
1040
1022
  entity: entityType,
1041
1023
  field: key,
1042
1024
  tokens: tokens.tokens,
1043
1025
  hashes,
1044
- applied,
1026
+ applied: true,
1045
1027
  tenantId: search.tenantId ?? null,
1046
1028
  organizationScope: search.organizationScope
1047
1029
  });
1048
- if (applied) return q;
1030
+ return applied;
1049
1031
  } else {
1050
- this.logSearchDebug("search:cf-skip-empty-hashes", {
1051
- entity: entityType,
1052
- field: key,
1053
- value
1054
- });
1032
+ this.logSearchDebug("search:cf-skip-empty-hashes", { entity: entityType, field: key, value });
1055
1033
  }
1056
1034
  return q;
1057
1035
  }
1058
1036
  switch (op) {
1059
1037
  case "eq":
1060
- return q.where((builder) => {
1061
- builder.orWhere(text, "=", value);
1062
- builder.orWhere(arrContains(value));
1063
- });
1038
+ return q.where((eb) => eb.or([
1039
+ sql`${textExpr} = ${value}`,
1040
+ arrContains(value)
1041
+ ]));
1064
1042
  case "ne":
1065
- return q.whereNot(text, "=", value);
1043
+ return q.where(sql`${textExpr} <> ${value}`);
1066
1044
  case "in": {
1067
1045
  const vals = this.toArray(value);
1068
- return q.where((builder) => {
1069
- vals.forEach((val) => {
1070
- builder.orWhere(text, "=", val);
1071
- builder.orWhere(arrContains(val));
1072
- });
1073
- });
1046
+ return q.where((eb) => eb.or(
1047
+ vals.flatMap((val) => [
1048
+ sql`${textExpr} = ${val}`,
1049
+ arrContains(val)
1050
+ ])
1051
+ ));
1074
1052
  }
1075
1053
  case "nin": {
1076
1054
  const vals = this.toArray(value);
1077
- return q.whereNotIn(text, vals);
1055
+ return q.where(sql`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
1078
1056
  }
1079
1057
  case "like":
1080
- return q.where(text, "like", value);
1058
+ return q.where(sql`${textExpr} like ${value}`);
1081
1059
  case "ilike":
1082
- return q.where(text, "ilike", value);
1060
+ return q.where(sql`${textExpr} ilike ${value}`);
1083
1061
  case "exists":
1084
- return value ? q.whereRaw(`${text.toString()} is not null`) : q.whereRaw(`${text.toString()} is null`);
1062
+ return value ? q.where(sql`${textExpr} is not null`) : q.where(sql`${textExpr} is null`);
1085
1063
  case "gt":
1086
1064
  case "gte":
1087
1065
  case "lt":
1088
1066
  case "lte": {
1089
- const operator = op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=";
1090
- return q.where(text, operator, value);
1067
+ const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
1068
+ return q.where(sql`${textExpr} ${operator} ${value}`);
1091
1069
  }
1092
1070
  default:
1093
1071
  return q;
1094
1072
  }
1095
1073
  }
1096
- applyIndexDocFilterFromAlias(knex, q, alias, entityType, key, op, value, recordIdColumn, search) {
1097
- const text = knex.raw(`(${alias}.doc ->> ?)`, [key]);
1074
+ applyIndexDocFilterFromAlias(q, alias, entityType, key, op, value, recordIdColumn, search) {
1075
+ const textExpr = sql`(${sql.ref(alias + ".doc")} ->> ${key})`;
1098
1076
  if ((op === "like" || op === "ilike") && search?.enabled && typeof value === "string") {
1099
1077
  const tokens = tokenizeText(String(value), search.config);
1100
1078
  const hashes = tokens.hashes;
1101
1079
  if (hashes.length) {
1102
- const applied = this.applySearchTokens(q, {
1103
- knex,
1080
+ const applied = q.where((eb) => eb.exists(this.buildSearchTokensSub(eb, {
1104
1081
  entity: entityType,
1105
1082
  field: key,
1106
1083
  hashes,
1107
1084
  recordIdColumn,
1108
1085
  tenantId: search.tenantId ?? null,
1109
1086
  organizationScope: search.organizationScope ?? null
1110
- });
1087
+ })));
1111
1088
  this.logSearchDebug("search:index-doc-filter", {
1112
1089
  entity: entityType,
1113
1090
  field: key,
1114
1091
  tokens: tokens.tokens,
1115
1092
  hashes,
1116
- applied,
1093
+ applied: true,
1117
1094
  tenantId: search.tenantId ?? null,
1118
1095
  organizationScope: search.organizationScope
1119
1096
  });
1120
- if (applied) return q;
1097
+ return applied;
1121
1098
  } else {
1122
- this.logSearchDebug("search:index-doc-skip-empty-hashes", {
1123
- entity: entityType,
1124
- field: key,
1125
- value
1126
- });
1099
+ this.logSearchDebug("search:index-doc-skip-empty-hashes", { entity: entityType, field: key, value });
1127
1100
  }
1128
1101
  return q;
1129
1102
  }
1130
1103
  switch (op) {
1131
1104
  case "eq":
1132
- return q.where(text, "=", value);
1105
+ return q.where(sql`${textExpr} = ${value}`);
1133
1106
  case "ne":
1134
- return q.where(text, "!=", value);
1107
+ return q.where(sql`${textExpr} <> ${value}`);
1108
+ case "in": {
1109
+ const vals = this.toArray(value);
1110
+ return q.where(sql`${textExpr} in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
1111
+ }
1112
+ case "nin": {
1113
+ const vals = this.toArray(value);
1114
+ return q.where(sql`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
1115
+ }
1116
+ case "like":
1117
+ return q.where(sql`${textExpr} like ${value}`);
1118
+ case "ilike":
1119
+ return q.where(sql`${textExpr} ilike ${value}`);
1120
+ case "exists":
1121
+ return value ? q.where(sql`${textExpr} is not null`) : q.where(sql`${textExpr} is null`);
1122
+ case "gt":
1123
+ case "gte":
1124
+ case "lt":
1125
+ case "lte": {
1126
+ const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
1127
+ return q.where(sql`${textExpr} ${operator} ${value}`);
1128
+ }
1129
+ default:
1130
+ return q;
1131
+ }
1132
+ }
1133
+ /**
1134
+ * Build a single OR-group base filter expression as a Kysely predicate
1135
+ * (no side effects on the outer builder).
1136
+ */
1137
+ buildBaseFilterExpression(eb, filter, resolveBaseColumn, qualify, entity, searchRuntime) {
1138
+ const fieldName = String(filter.field);
1139
+ const baseField = resolveBaseColumn(fieldName);
1140
+ if (!baseField) {
1141
+ return this.buildIndexDocFilterExpression(eb, "ei", entity, fieldName, filter.op, filter.value, "b.id", searchRuntime);
1142
+ }
1143
+ return this.buildColumnFilterExpression(eb, qualify(baseField), filter.op, filter.value);
1144
+ }
1145
+ buildColumnFilterExpression(eb, column, op, value) {
1146
+ switch (op) {
1147
+ case "eq":
1148
+ return eb(column, "=", value);
1149
+ case "ne":
1150
+ return eb(column, "!=", value);
1151
+ case "gt":
1152
+ return eb(column, ">", value);
1153
+ case "gte":
1154
+ return eb(column, ">=", value);
1155
+ case "lt":
1156
+ return eb(column, "<", value);
1157
+ case "lte":
1158
+ return eb(column, "<=", value);
1135
1159
  case "in":
1136
- return q.whereIn(text, this.toArray(value));
1160
+ return eb(column, "in", this.toArray(value));
1137
1161
  case "nin":
1138
- return q.whereNotIn(text, this.toArray(value));
1162
+ return eb(column, "not in", this.toArray(value));
1139
1163
  case "like":
1140
- return q.where(text, "like", value);
1164
+ return eb(column, "like", value);
1141
1165
  case "ilike":
1142
- return q.where(text, "ilike", value);
1166
+ return eb(column, "ilike", value);
1143
1167
  case "exists":
1144
- return value ? q.whereRaw(`${text.toString()} is not null`) : q.whereRaw(`${text.toString()} is null`);
1168
+ return eb(column, value ? "is not" : "is", null);
1169
+ default:
1170
+ return sql`true`;
1171
+ }
1172
+ }
1173
+ buildIndexDocFilterExpression(eb, alias, _entity, key, op, value, _recordIdColumn, _search) {
1174
+ const textExpr = sql`(${sql.ref(alias + ".doc")} ->> ${key})`;
1175
+ switch (op) {
1176
+ case "eq":
1177
+ return sql`${textExpr} = ${value}`;
1178
+ case "ne":
1179
+ return sql`${textExpr} <> ${value}`;
1145
1180
  case "gt":
1146
1181
  case "gte":
1147
1182
  case "lt":
1148
1183
  case "lte": {
1149
- const operator = op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=";
1150
- return q.where(text, operator, value);
1184
+ const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
1185
+ return sql`${textExpr} ${operator} ${value}`;
1186
+ }
1187
+ case "like":
1188
+ return sql`${textExpr} like ${value}`;
1189
+ case "ilike":
1190
+ return sql`${textExpr} ilike ${value}`;
1191
+ case "in": {
1192
+ const vals = this.toArray(value);
1193
+ return sql`${textExpr} in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`;
1151
1194
  }
1195
+ case "nin": {
1196
+ const vals = this.toArray(value);
1197
+ return sql`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`;
1198
+ }
1199
+ case "exists":
1200
+ return value ? sql`${textExpr} is not null` : sql`${textExpr} is null`;
1152
1201
  default:
1153
- return q;
1202
+ return sql`true`;
1154
1203
  }
1155
1204
  }
1156
1205
  async queryCustomEntity(entity, opts = {}) {
1157
- const knex = this.getKnex();
1206
+ const db = this.getDb();
1158
1207
  const alias = "ce";
1159
- let q = knex({ [alias]: "custom_entities_storage" }).where(`${alias}.entity_type`, entity);
1160
1208
  const orgScope = this.resolveOrganizationScope(opts);
1161
1209
  if (!opts.tenantId) throw new Error("QueryEngine: tenantId is required");
1162
- q = q.andWhere(`${alias}.tenant_id`, opts.tenantId);
1163
- if (orgScope) {
1164
- q = this.applyOrganizationScope(q, `${alias}.organization_id`, orgScope);
1165
- }
1166
- if (!opts.withDeleted) q = q.whereNull(`${alias}.deleted_at`);
1167
1210
  const searchConfig = resolveSearchConfig();
1168
1211
  const searchEnabled = searchConfig.enabled && await this.tableExists("search_tokens");
1169
1212
  const hasSearchTokens = searchEnabled ? await this.hasSearchTokens(entity, opts.tenantId ?? null, orgScope) : false;
@@ -1174,31 +1217,37 @@ class HybridQueryEngine {
1174
1217
  tenantId: opts.tenantId ?? null
1175
1218
  };
1176
1219
  const normalizedFilters = normalizeFilters(opts.filters);
1177
- for (const filter of normalizedFilters) {
1178
- if (filter.field.startsWith("cf:")) {
1179
- q = this.applyCfFilterFromAlias(knex, q, alias, entity, filter.field, filter.op, filter.value, searchRuntime);
1180
- continue;
1220
+ const applyScope = (q) => {
1221
+ let next = q.where(`${alias}.entity_type`, "=", entity).where(`${alias}.tenant_id`, "=", opts.tenantId);
1222
+ if (orgScope) {
1223
+ next = this.applyOrganizationScope(next, `${alias}.organization_id`, orgScope);
1181
1224
  }
1182
- const column = this.resolveCustomEntityColumn(alias, String(filter.field));
1183
- if (column) {
1184
- q = this.applyColumnFilter(q, column, filter, {
1225
+ if (!opts.withDeleted) next = next.where(`${alias}.deleted_at`, "is", null);
1226
+ for (const filter of normalizedFilters) {
1227
+ if (filter.field.startsWith("cf:")) {
1228
+ next = this.applyCfFilterFromAlias(next, alias, entity, filter.field, filter.op, filter.value, searchRuntime);
1229
+ continue;
1230
+ }
1231
+ const column = this.resolveCustomEntityColumn(alias, String(filter.field));
1232
+ if (column) {
1233
+ next = this.applyColumnFilter(next, column, filter, {
1234
+ ...searchRuntime,
1235
+ entity,
1236
+ field: String(filter.field),
1237
+ recordIdColumn: `${alias}.entity_id`
1238
+ });
1239
+ continue;
1240
+ }
1241
+ const docExpr = sql`(${sql.ref(alias + ".doc")} ->> ${String(filter.field)})`;
1242
+ next = this.applyColumnFilter(next, docExpr, filter, {
1185
1243
  ...searchRuntime,
1186
- knex,
1187
1244
  entity,
1188
1245
  field: String(filter.field),
1189
1246
  recordIdColumn: `${alias}.entity_id`
1190
1247
  });
1191
- continue;
1192
1248
  }
1193
- const docExpr = knex.raw(`(${alias}.doc ->> ?)`, [String(filter.field)]);
1194
- q = this.applyColumnFilter(q, docExpr, filter, {
1195
- ...searchRuntime,
1196
- knex,
1197
- entity,
1198
- field: String(filter.field),
1199
- recordIdColumn: `${alias}.entity_id`
1200
- });
1201
- }
1249
+ return next;
1250
+ };
1202
1251
  const cfKeys = /* @__PURE__ */ new Set();
1203
1252
  for (const f of opts.fields || []) {
1204
1253
  if (typeof f === "string" && f.startsWith("cf:")) cfKeys.add(f.slice(3));
@@ -1210,90 +1259,87 @@ class HybridQueryEngine {
1210
1259
  }
1211
1260
  if (opts.includeCustomFields === true) {
1212
1261
  try {
1213
- const rows = await knex("custom_field_defs").select("key").where({ entity_id: entity, is_active: true }).modify((qb) => {
1214
- qb.andWhere({ tenant_id: opts.tenantId });
1215
- });
1262
+ const rows = await db.selectFrom("custom_field_defs").select("key").where("entity_id", "=", entity).where("is_active", "=", true).where("tenant_id", "=", opts.tenantId).execute();
1216
1263
  for (const row of rows) {
1217
1264
  const key = row.key;
1218
- if (typeof key === "string") {
1219
- cfKeys.add(key);
1220
- } else if (key != null) {
1221
- cfKeys.add(String(key));
1222
- }
1265
+ if (typeof key === "string") cfKeys.add(key);
1266
+ else if (key != null) cfKeys.add(String(key));
1223
1267
  }
1224
1268
  } catch {
1225
1269
  }
1226
1270
  } else if (Array.isArray(opts.includeCustomFields)) {
1227
1271
  for (const k of opts.includeCustomFields) cfKeys.add(k);
1228
1272
  }
1229
- const requested = opts.fields && opts.fields.length ? opts.fields : ["id"];
1230
- for (const field of requested) {
1231
- const f = String(field);
1232
- if (f.startsWith("cf:")) {
1233
- const aliasName = this.sanitize(f);
1234
- const expr = this.jsonbRawAlias(knex, alias, f);
1235
- q = q.select({ [aliasName]: expr });
1236
- } else if (f === "id") {
1237
- q = q.select(knex.raw(`${alias}.entity_id as ??`, ["id"]));
1238
- } else if (f === "created_at" || f === "updated_at" || f === "deleted_at") {
1239
- q = q.select(knex.raw(`${alias}.?? as ??`, [f, f]));
1240
- } else {
1241
- const expr = knex.raw(`(${alias}.doc ->> ?)`, [f]);
1242
- q = q.select({ [f]: expr });
1273
+ const applySelection = (q) => {
1274
+ let next = q;
1275
+ const requested = opts.fields && opts.fields.length ? opts.fields : ["id"];
1276
+ for (const field of requested) {
1277
+ const f = String(field);
1278
+ if (f.startsWith("cf:")) {
1279
+ const aliasName = this.sanitize(f);
1280
+ next = next.select(this.jsonbSqlAlias(alias, f).as(aliasName));
1281
+ } else if (f === "id") {
1282
+ next = next.select(`${alias}.entity_id as id`);
1283
+ } else if (f === "created_at" || f === "updated_at" || f === "deleted_at") {
1284
+ next = next.select(`${alias}.${f} as ${f}`);
1285
+ } else {
1286
+ const expr = sql`(${sql.ref(alias + ".doc")} ->> ${f})`;
1287
+ next = next.select(expr.as(f));
1288
+ }
1243
1289
  }
1244
- }
1245
- const cfSelectedAliases = [];
1246
- for (const key of cfKeys) {
1247
- const aliasName = this.sanitize(`cf:${key}`);
1248
- const expr = this.jsonbRawAlias(knex, alias, `cf:${key}`);
1249
- q = q.select({ [aliasName]: expr });
1250
- cfSelectedAliases.push(aliasName);
1251
- }
1252
- for (const s of opts.sort || []) {
1253
- if (s.field.startsWith("cf:")) {
1254
- const key = s.field.slice(3);
1290
+ for (const key of cfKeys) {
1255
1291
  const aliasName = this.sanitize(`cf:${key}`);
1256
- if (!cfSelectedAliases.includes(aliasName)) {
1257
- const expr = this.jsonbRawAlias(knex, alias, `cf:${key}`);
1258
- q = q.select({ [aliasName]: expr });
1259
- cfSelectedAliases.push(aliasName);
1292
+ next = next.select(this.jsonbSqlAlias(alias, `cf:${key}`).as(aliasName));
1293
+ }
1294
+ return next;
1295
+ };
1296
+ const applySort = (q) => {
1297
+ let next = q;
1298
+ for (const s of opts.sort || []) {
1299
+ if (s.field.startsWith("cf:")) {
1300
+ const key = s.field.slice(3);
1301
+ const aliasName = this.sanitize(`cf:${key}`);
1302
+ next = next.orderBy(aliasName, s.dir ?? SortDir.Asc);
1303
+ } else if (s.field === "id") {
1304
+ next = next.orderBy(`${alias}.entity_id`, s.dir ?? SortDir.Asc);
1305
+ } else if (s.field === "created_at" || s.field === "updated_at" || s.field === "deleted_at") {
1306
+ next = next.orderBy(`${alias}.${s.field}`, s.dir ?? SortDir.Asc);
1307
+ } else {
1308
+ const direction = sql.raw(String(s.dir ?? SortDir.Asc));
1309
+ next = next.orderBy(sql`(${sql.ref(alias + ".doc")} ->> ${s.field}) ${direction}`);
1260
1310
  }
1261
- q = q.orderBy(aliasName, s.dir ?? SortDir.Asc);
1262
- } else if (s.field === "id") {
1263
- q = q.orderBy(`${alias}.entity_id`, s.dir ?? SortDir.Asc);
1264
- } else if (s.field === "created_at" || s.field === "updated_at" || s.field === "deleted_at") {
1265
- q = q.orderBy(`${alias}.${s.field}`, s.dir ?? SortDir.Asc);
1266
- } else {
1267
- const direction = s.dir ?? SortDir.Asc;
1268
- q = q.orderByRaw(`(${alias}.doc ->> ?) ${direction}`, [s.field]);
1269
1311
  }
1270
- }
1312
+ return next;
1313
+ };
1271
1314
  const page = opts.page?.page ?? 1;
1272
1315
  const pageSize = opts.page?.pageSize ?? 20;
1273
- const countClone = q.clone();
1274
- if (typeof countClone.clearSelect === "function") countClone.clearSelect();
1275
- if (typeof countClone.clearOrder === "function") countClone.clearOrder();
1276
- const countRow = await countClone.countDistinct(`${alias}.entity_id as count`).first();
1316
+ const root = db.selectFrom(`custom_entities_storage as ${alias}`);
1317
+ const countQuery = applyScope(root).select(sql`count(distinct ${sql.ref(`${alias}.entity_id`)})`.as("count"));
1318
+ const countRow = await countQuery.executeTakeFirst();
1277
1319
  const total = this.parseCount(countRow);
1278
- const items = await q.limit(pageSize).offset((page - 1) * pageSize);
1320
+ let dataQuery = applyScope(db.selectFrom(`custom_entities_storage as ${alias}`));
1321
+ dataQuery = applySelection(dataQuery);
1322
+ dataQuery = applySort(dataQuery);
1323
+ dataQuery = dataQuery.limit(pageSize).offset((page - 1) * pageSize);
1324
+ const items = await dataQuery.execute();
1279
1325
  return { items, page, pageSize, total };
1280
1326
  }
1281
1327
  async tableExists(table) {
1282
- const knex = this.getKnex();
1283
- const exists = await knex("information_schema.tables").where({ table_name: table }).first();
1328
+ const db = this.getDb();
1329
+ const exists = await db.selectFrom("information_schema.tables").select(sql`1`.as("one")).where("table_name", "=", table).executeTakeFirst();
1284
1330
  return !!exists;
1285
1331
  }
1286
1332
  async hasSearchTokens(entity, tenantId, orgScope) {
1287
1333
  try {
1288
- const knex = this.getKnex();
1289
- const query = knex("search_tokens").select(1).where("entity_type", entity).limit(1);
1334
+ const db = this.getDb();
1335
+ let query = db.selectFrom("search_tokens").select(sql`1`.as("one")).where("entity_type", "=", entity);
1290
1336
  if (tenantId !== void 0) {
1291
- query.andWhereRaw("tenant_id is not distinct from ?", [tenantId]);
1337
+ query = query.where(sql`tenant_id is not distinct from ${tenantId}`);
1292
1338
  }
1293
1339
  if (orgScope) {
1294
- this.applyOrganizationScope(query, "search_tokens.organization_id", orgScope);
1340
+ query = this.applyOrganizationScope(query, "search_tokens.organization_id", orgScope);
1295
1341
  }
1296
- const row = await query.first();
1342
+ const row = await query.limit(1).executeTakeFirst();
1297
1343
  return !!row;
1298
1344
  } catch (err) {
1299
1345
  this.logSearchDebug("search:has-tokens-error", {
@@ -1324,17 +1370,14 @@ class HybridQueryEngine {
1324
1370
  const cacheKey = this.customFieldKeysCacheKey(entityIds, tenantId);
1325
1371
  const now = Date.now();
1326
1372
  const cached = this.customFieldKeysCache.get(cacheKey);
1327
- if (cached && cached.expiresAt > now) {
1328
- return cached.value.slice();
1329
- }
1330
- const knex = this.getKnex();
1331
- const rows = await knex("custom_field_defs").select("key").whereIn("entity_id", entityIds).andWhere("is_active", true).modify((qb) => {
1332
- qb.andWhere((inner) => {
1333
- inner.where({ tenant_id: tenantId }).orWhereNull("tenant_id");
1334
- });
1335
- });
1373
+ if (cached && cached.expiresAt > now) return cached.value.slice();
1374
+ const db = this.getDb();
1375
+ const rows = await db.selectFrom("custom_field_defs").select("key").where("entity_id", "in", entityIds).where("is_active", "=", true).where((eb) => eb.or([
1376
+ eb("tenant_id", "=", tenantId),
1377
+ eb("tenant_id", "is", null)
1378
+ ])).execute();
1336
1379
  const keys = /* @__PURE__ */ new Set();
1337
- for (const row of rows || []) {
1380
+ for (const row of rows) {
1338
1381
  const key = row.key;
1339
1382
  if (typeof key === "string" && key.trim().length) keys.add(key.trim());
1340
1383
  else if (key != null) keys.add(String(key));
@@ -1376,27 +1419,24 @@ class HybridQueryEngine {
1376
1419
  return entity;
1377
1420
  }
1378
1421
  async indexAnyRows(entity) {
1379
- const knex = this.getKnex();
1380
- const coverage = await knex("entity_index_coverage").select(1).where("entity_type", entity).where("indexed_count", ">", 0).first();
1422
+ const db = this.getDb();
1423
+ const coverage = await db.selectFrom("entity_index_coverage").select(sql`1`.as("one")).where("entity_type", "=", entity).where("indexed_count", ">", 0).executeTakeFirst();
1381
1424
  if (coverage) return true;
1382
- const exists = await knex("entity_indexes").select("entity_id").where({ entity_type: entity }).first();
1425
+ const exists = await db.selectFrom("entity_indexes").select("entity_id").where("entity_type", "=", entity).executeTakeFirst();
1383
1426
  return !!exists;
1384
1427
  }
1385
1428
  async getStoredCoverageSnapshot(entity, tenantId, organizationId, withDeleted) {
1386
1429
  try {
1387
1430
  if (!this.isCoverageOptimizationEnabled()) {
1388
- await refreshCoverageSnapshot(
1389
- this.em,
1390
- {
1391
- entityType: entity,
1392
- tenantId,
1393
- organizationId,
1394
- withDeleted
1395
- }
1396
- );
1431
+ await refreshCoverageSnapshot(this.em, {
1432
+ entityType: entity,
1433
+ tenantId,
1434
+ organizationId,
1435
+ withDeleted
1436
+ });
1397
1437
  }
1398
- const knex = this.getKnex();
1399
- const row = await readCoverageSnapshot(knex, {
1438
+ const db = this.getDb();
1439
+ const row = await readCoverageSnapshot(db, {
1400
1440
  entityType: entity,
1401
1441
  tenantId,
1402
1442
  organizationId,
@@ -1427,13 +1467,7 @@ class HybridQueryEngine {
1427
1467
  organizationId: organizationIdOverride ?? opts.organizationId ?? null,
1428
1468
  force: false
1429
1469
  };
1430
- const context = stats ? {
1431
- entity,
1432
- tenantId: payload.tenantId,
1433
- organizationId: payload.organizationId,
1434
- baseCount: stats.baseCount,
1435
- indexedCount: stats.indexedCount
1436
- } : { entity, tenantId: payload.tenantId, organizationId: payload.organizationId };
1470
+ const context = stats ? { entity, tenantId: payload.tenantId, organizationId: payload.organizationId, baseCount: stats.baseCount, indexedCount: stats.indexedCount } : { entity, tenantId: payload.tenantId, organizationId: payload.organizationId };
1437
1471
  void Promise.resolve().then(async () => {
1438
1472
  try {
1439
1473
  await bus.emitEvent("query_index.reindex", payload, { persistent: true });
@@ -1449,12 +1483,7 @@ class HybridQueryEngine {
1449
1483
  scheduleCoverageRefresh(entity, tenantId, organizationId, withDeleted) {
1450
1484
  const bus = this.resolveEventBus();
1451
1485
  if (!bus) return;
1452
- const key = [
1453
- entity,
1454
- tenantId ?? "__tenant__",
1455
- organizationId ?? "__org__",
1456
- withDeleted ? "1" : "0"
1457
- ].join("|");
1486
+ const key = [entity, tenantId ?? "__tenant__", organizationId ?? "__org__", withDeleted ? "1" : "0"].join("|");
1458
1487
  if (this.pendingCoverageRefreshKeys.has(key)) return;
1459
1488
  this.pendingCoverageRefreshKeys.add(key);
1460
1489
  void Promise.resolve().then(async () => {
@@ -1526,17 +1555,17 @@ class HybridQueryEngine {
1526
1555
  if (cached === true) return true;
1527
1556
  this.columnCache.delete(key);
1528
1557
  }
1529
- const knex = this.getKnex();
1530
- const exists = await knex("information_schema.columns").where({ table_name: table, column_name: column }).first();
1558
+ const db = this.getDb();
1559
+ const exists = await db.selectFrom("information_schema.columns").select(sql`1`.as("one")).where("table_name", "=", table).where("column_name", "=", column).executeTakeFirst();
1531
1560
  const present = !!exists;
1532
1561
  if (present) this.columnCache.set(key, true);
1533
1562
  else this.columnCache.delete(key);
1534
1563
  return present;
1535
1564
  }
1536
1565
  async getBaseColumnsForEntity(entity) {
1537
- const knex = this.getKnex();
1566
+ const db = this.getDb();
1538
1567
  const table = resolveEntityTableName(this.em, entity);
1539
- const rows = await knex("information_schema.columns").select("column_name", "data_type").where({ table_name: table });
1568
+ const rows = await db.selectFrom("information_schema.columns").select(["column_name", "data_type"]).where("table_name", "=", table).execute();
1540
1569
  const map = /* @__PURE__ */ new Map();
1541
1570
  for (const r of rows) map.set(r.column_name, r.data_type);
1542
1571
  return map;
@@ -1568,20 +1597,14 @@ class HybridQueryEngine {
1568
1597
  }
1569
1598
  applyOrganizationScope(q, column, scope) {
1570
1599
  if (scope.ids.length === 0 && !scope.includeNull) {
1571
- return q.whereRaw("1 = 0");
1600
+ return q.where(sql`1 = 0`);
1572
1601
  }
1573
- return q.where((builder) => {
1574
- let applied = false;
1575
- if (scope.ids.length > 0) {
1576
- builder.whereIn(column, scope.ids);
1577
- applied = true;
1578
- }
1579
- if (scope.includeNull) {
1580
- if (applied) builder.orWhereNull(column);
1581
- else builder.whereNull(column);
1582
- } else if (!applied) {
1583
- builder.whereRaw("1 = 0");
1584
- }
1602
+ return q.where((eb) => {
1603
+ const parts = [];
1604
+ if (scope.ids.length > 0) parts.push(eb(column, "in", scope.ids));
1605
+ if (scope.includeNull) parts.push(eb(column, "is", null));
1606
+ if (parts.length === 1) return parts[0];
1607
+ return eb.or(parts);
1585
1608
  });
1586
1609
  }
1587
1610
  normalizeFilters(filters) {
@@ -1647,12 +1670,8 @@ class HybridQueryEngine {
1647
1670
  return s.replace(/[^a-zA-Z0-9_]/g, "_");
1648
1671
  }
1649
1672
  toArray(value) {
1650
- if (Array.isArray(value)) {
1651
- return value;
1652
- }
1653
- if (value === void 0) {
1654
- return [];
1655
- }
1673
+ if (Array.isArray(value)) return value;
1674
+ if (value === void 0) return [];
1656
1675
  return [value];
1657
1676
  }
1658
1677
  parseCount(row) {
@@ -1663,6 +1682,7 @@ class HybridQueryEngine {
1663
1682
  const parsed = Number(value);
1664
1683
  return Number.isNaN(parsed) ? 0 : parsed;
1665
1684
  }
1685
+ if (typeof value === "bigint") return Number(value);
1666
1686
  }
1667
1687
  return 0;
1668
1688
  }
@@ -1680,35 +1700,30 @@ class HybridQueryEngine {
1680
1700
  const hashes = tokens.hashes;
1681
1701
  if (hashes.length) {
1682
1702
  const sources = (search.searchSources && search.searchSources.length ? search.searchSources : [{ entity: search.entity, recordIdColumn: search.recordIdColumn ?? "" }]).filter((src) => src.recordIdColumn && src.entity);
1683
- let applied = false;
1684
1703
  if (sources.length) {
1685
- q = q.where((qb) => {
1686
- sources.forEach((src, idx) => {
1687
- const ok = this.applySearchTokens(qb, {
1688
- knex: search.knex,
1689
- entity: src.entity,
1690
- field: search.field,
1691
- hashes,
1692
- recordIdColumn: src.recordIdColumn,
1693
- tenantId: search.tenantId ?? null,
1694
- organizationScope: search.organizationScope ?? null,
1695
- combineWith: idx === 0 ? "and" : "or"
1696
- });
1697
- if (ok) applied = true;
1698
- });
1704
+ const engine = this;
1705
+ q = q.where((eb) => eb.or(
1706
+ sources.map((src) => eb.exists(engine.buildSearchTokensSub(eb, {
1707
+ entity: src.entity,
1708
+ field: search.field,
1709
+ hashes,
1710
+ recordIdColumn: src.recordIdColumn,
1711
+ tenantId: search.tenantId ?? null,
1712
+ organizationScope: search.organizationScope ?? null
1713
+ })))
1714
+ ));
1715
+ this.logSearchDebug("search:filter", {
1716
+ entity: search.entity,
1717
+ field: search.field,
1718
+ tokens: tokens.tokens,
1719
+ hashes,
1720
+ applied: true,
1721
+ tenantId: search.tenantId ?? null,
1722
+ organizationScope: search.organizationScope,
1723
+ sources: sources.map((src) => ({ entity: src.entity, recordIdColumn: src.recordIdColumn }))
1699
1724
  });
1725
+ return q;
1700
1726
  }
1701
- this.logSearchDebug("search:filter", {
1702
- entity: search.entity,
1703
- field: search.field,
1704
- tokens: tokens.tokens,
1705
- hashes,
1706
- applied,
1707
- tenantId: search.tenantId ?? null,
1708
- organizationScope: search.organizationScope,
1709
- sources: sources.map((src) => ({ entity: src.entity, recordIdColumn: src.recordIdColumn }))
1710
- });
1711
- if (applied) return q;
1712
1727
  } else {
1713
1728
  this.logSearchDebug("search:skip-empty-hashes", {
1714
1729
  entity: search.entity,
@@ -1721,9 +1736,9 @@ class HybridQueryEngine {
1721
1736
  const col = column;
1722
1737
  switch (filter.op) {
1723
1738
  case "eq":
1724
- return q.where(col, filter.value);
1739
+ return q.where(col, "=", filter.value);
1725
1740
  case "ne":
1726
- return q.whereNot(col, filter.value);
1741
+ return q.where(col, "!=", filter.value);
1727
1742
  case "gt":
1728
1743
  case "gte":
1729
1744
  case "lt":
@@ -1731,20 +1746,16 @@ class HybridQueryEngine {
1731
1746
  const operator = filter.op === "gt" ? ">" : filter.op === "gte" ? ">=" : filter.op === "lt" ? "<" : "<=";
1732
1747
  return q.where(col, operator, filter.value);
1733
1748
  }
1734
- case "in": {
1735
- const values = this.toArray(filter.value);
1736
- return q.whereIn(col, values);
1737
- }
1738
- case "nin": {
1739
- const values = this.toArray(filter.value);
1740
- return q.whereNotIn(col, values);
1741
- }
1749
+ case "in":
1750
+ return q.where(col, "in", this.toArray(filter.value));
1751
+ case "nin":
1752
+ return q.where(col, "not in", this.toArray(filter.value));
1742
1753
  case "like":
1743
1754
  return q.where(col, "like", filter.value);
1744
1755
  case "ilike":
1745
1756
  return q.where(col, "ilike", filter.value);
1746
1757
  case "exists":
1747
- return filter.value ? q.whereNotNull(col) : q.whereNull(col);
1758
+ return filter.value ? q.where(col, "is not", null) : q.where(col, "is", null);
1748
1759
  default:
1749
1760
  return q;
1750
1761
  }
@@ -1785,9 +1796,7 @@ class HybridQueryEngine {
1785
1796
  const baseCount = snapshot.baseCount;
1786
1797
  const indexCount = snapshot.indexedCount;
1787
1798
  const hasGap = baseCount > 0 && indexCount < baseCount;
1788
- if (hasGap || indexCount > baseCount) {
1789
- return { stats: snapshot, scope: "scoped" };
1790
- }
1799
+ if (hasGap || indexCount > baseCount) return { stats: snapshot, scope: "scoped" };
1791
1800
  return null;
1792
1801
  }
1793
1802
  // Backward-compatible hook for tests that mock coverage stats
@@ -1798,18 +1807,13 @@ class HybridQueryEngine {
1798
1807
  async captureSqlTiming(label, entity, execute, extra, profiler) {
1799
1808
  const shouldDebug = this.isSqlDebugEnabled() && this.isDebugVerbosity();
1800
1809
  const shouldProfile = profiler?.enabled === true;
1801
- if (!shouldDebug && !shouldProfile) {
1802
- return Promise.resolve(execute());
1803
- }
1810
+ if (!shouldDebug && !shouldProfile) return Promise.resolve(execute());
1804
1811
  const startedAt = process.hrtime.bigint();
1805
1812
  try {
1806
1813
  return await Promise.resolve(execute());
1807
1814
  } finally {
1808
1815
  const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
1809
- const context = {
1810
- entity,
1811
- durationMs: Math.round(elapsedMs * 1e3) / 1e3
1812
- };
1816
+ const context = { entity, durationMs: Math.round(elapsedMs * 1e3) / 1e3 };
1813
1817
  if (extra) Object.assign(context, extra);
1814
1818
  if (shouldProfile) profiler.record(label, context.durationMs, extra);
1815
1819
  if (shouldDebug) this.debug(`${label}:timing`, context);