@open-mercato/shared 0.4.2-canary-c02407ff85

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 (324) hide show
  1. package/build.mjs +101 -0
  2. package/dist/index.js +1 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/lib/api/crud.js +47 -0
  5. package/dist/lib/api/crud.js.map +7 -0
  6. package/dist/lib/api/scoped.js +140 -0
  7. package/dist/lib/api/scoped.js.map +7 -0
  8. package/dist/lib/auth/jwt.js +34 -0
  9. package/dist/lib/auth/jwt.js.map +7 -0
  10. package/dist/lib/auth/server.js +157 -0
  11. package/dist/lib/auth/server.js.map +7 -0
  12. package/dist/lib/boolean.js +22 -0
  13. package/dist/lib/boolean.js.map +7 -0
  14. package/dist/lib/bootstrap/appResolver.js +43 -0
  15. package/dist/lib/bootstrap/appResolver.js.map +7 -0
  16. package/dist/lib/bootstrap/dynamicLoader.js +108 -0
  17. package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
  18. package/dist/lib/bootstrap/factory.js +59 -0
  19. package/dist/lib/bootstrap/factory.js.map +7 -0
  20. package/dist/lib/bootstrap/index.js +11 -0
  21. package/dist/lib/bootstrap/index.js.map +7 -0
  22. package/dist/lib/bootstrap/types.js +1 -0
  23. package/dist/lib/bootstrap/types.js.map +7 -0
  24. package/dist/lib/cache/segments.js +36 -0
  25. package/dist/lib/cache/segments.js.map +7 -0
  26. package/dist/lib/cli/progress.js +46 -0
  27. package/dist/lib/cli/progress.js.map +7 -0
  28. package/dist/lib/commands/command-bus.js +285 -0
  29. package/dist/lib/commands/command-bus.js.map +7 -0
  30. package/dist/lib/commands/customFieldSnapshots.js +66 -0
  31. package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
  32. package/dist/lib/commands/helpers.js +98 -0
  33. package/dist/lib/commands/helpers.js.map +7 -0
  34. package/dist/lib/commands/index.js +8 -0
  35. package/dist/lib/commands/index.js.map +7 -0
  36. package/dist/lib/commands/operationMetadata.js +32 -0
  37. package/dist/lib/commands/operationMetadata.js.map +7 -0
  38. package/dist/lib/commands/registry.js +43 -0
  39. package/dist/lib/commands/registry.js.map +7 -0
  40. package/dist/lib/commands/scope.js +44 -0
  41. package/dist/lib/commands/scope.js.map +7 -0
  42. package/dist/lib/commands/types.js +8 -0
  43. package/dist/lib/commands/types.js.map +7 -0
  44. package/dist/lib/crud/cache-stats.js +98 -0
  45. package/dist/lib/crud/cache-stats.js.map +7 -0
  46. package/dist/lib/crud/cache.js +175 -0
  47. package/dist/lib/crud/cache.js.map +7 -0
  48. package/dist/lib/crud/custom-fields-client.js +52 -0
  49. package/dist/lib/crud/custom-fields-client.js.map +7 -0
  50. package/dist/lib/crud/custom-fields.js +467 -0
  51. package/dist/lib/crud/custom-fields.js.map +7 -0
  52. package/dist/lib/crud/errors.js +24 -0
  53. package/dist/lib/crud/errors.js.map +7 -0
  54. package/dist/lib/crud/exporters.js +154 -0
  55. package/dist/lib/crud/exporters.js.map +7 -0
  56. package/dist/lib/crud/factory.js +1311 -0
  57. package/dist/lib/crud/factory.js.map +7 -0
  58. package/dist/lib/crud/types.js +1 -0
  59. package/dist/lib/crud/types.js.map +7 -0
  60. package/dist/lib/custom-fields/normalize.js +36 -0
  61. package/dist/lib/custom-fields/normalize.js.map +7 -0
  62. package/dist/lib/data/engine.js +396 -0
  63. package/dist/lib/data/engine.js.map +7 -0
  64. package/dist/lib/db/escapeLikePattern.js +5 -0
  65. package/dist/lib/db/escapeLikePattern.js.map +7 -0
  66. package/dist/lib/db/mikro.js +82 -0
  67. package/dist/lib/db/mikro.js.map +7 -0
  68. package/dist/lib/di/container.js +94 -0
  69. package/dist/lib/di/container.js.map +7 -0
  70. package/dist/lib/email/send.js +12 -0
  71. package/dist/lib/email/send.js.map +7 -0
  72. package/dist/lib/encryption/aes.js +58 -0
  73. package/dist/lib/encryption/aes.js.map +7 -0
  74. package/dist/lib/encryption/customFieldValues.js +49 -0
  75. package/dist/lib/encryption/customFieldValues.js.map +7 -0
  76. package/dist/lib/encryption/entityFields.js +26 -0
  77. package/dist/lib/encryption/entityFields.js.map +7 -0
  78. package/dist/lib/encryption/entityIds.js +80 -0
  79. package/dist/lib/encryption/entityIds.js.map +7 -0
  80. package/dist/lib/encryption/find.js +45 -0
  81. package/dist/lib/encryption/find.js.map +7 -0
  82. package/dist/lib/encryption/indexDoc.js +69 -0
  83. package/dist/lib/encryption/indexDoc.js.map +7 -0
  84. package/dist/lib/encryption/kms.js +282 -0
  85. package/dist/lib/encryption/kms.js.map +7 -0
  86. package/dist/lib/encryption/subscriber.js +330 -0
  87. package/dist/lib/encryption/subscriber.js.map +7 -0
  88. package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
  89. package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
  90. package/dist/lib/encryption/toggles.js +18 -0
  91. package/dist/lib/encryption/toggles.js.map +7 -0
  92. package/dist/lib/entities/naming.js +9 -0
  93. package/dist/lib/entities/naming.js.map +7 -0
  94. package/dist/lib/entities/system-entities.js +43 -0
  95. package/dist/lib/entities/system-entities.js.map +7 -0
  96. package/dist/lib/frontend/organizationEvents.js +41 -0
  97. package/dist/lib/frontend/organizationEvents.js.map +7 -0
  98. package/dist/lib/frontend/useOrganizationScope.js +32 -0
  99. package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
  100. package/dist/lib/hotkeys/index.js +128 -0
  101. package/dist/lib/hotkeys/index.js.map +7 -0
  102. package/dist/lib/i18n/app-dictionaries.js +17 -0
  103. package/dist/lib/i18n/app-dictionaries.js.map +7 -0
  104. package/dist/lib/i18n/config.js +7 -0
  105. package/dist/lib/i18n/config.js.map +7 -0
  106. package/dist/lib/i18n/context.js +50 -0
  107. package/dist/lib/i18n/context.js.map +7 -0
  108. package/dist/lib/i18n/server.js +68 -0
  109. package/dist/lib/i18n/server.js.map +7 -0
  110. package/dist/lib/i18n/translate.js +45 -0
  111. package/dist/lib/i18n/translate.js.map +7 -0
  112. package/dist/lib/indexers/error-log.js +82 -0
  113. package/dist/lib/indexers/error-log.js.map +7 -0
  114. package/dist/lib/indexers/status-log.js +80 -0
  115. package/dist/lib/indexers/status-log.js.map +7 -0
  116. package/dist/lib/lib/auth/jwt.js +34 -0
  117. package/dist/lib/lib/auth/jwt.js.map +7 -0
  118. package/dist/lib/lib/auth/server.js +77 -0
  119. package/dist/lib/lib/auth/server.js.map +7 -0
  120. package/dist/lib/lib/email/send.js +12 -0
  121. package/dist/lib/lib/email/send.js.map +7 -0
  122. package/dist/lib/lib/i18n/config.js +7 -0
  123. package/dist/lib/lib/i18n/config.js.map +7 -0
  124. package/dist/lib/lib/i18n/context.js +31 -0
  125. package/dist/lib/lib/i18n/context.js.map +7 -0
  126. package/dist/lib/lib/utils.js +9 -0
  127. package/dist/lib/lib/utils.js.map +7 -0
  128. package/dist/lib/location/countries.js +68 -0
  129. package/dist/lib/location/countries.js.map +7 -0
  130. package/dist/lib/modules/index.js +6 -0
  131. package/dist/lib/modules/index.js.map +7 -0
  132. package/dist/lib/modules/registry.js +18 -0
  133. package/dist/lib/modules/registry.js.map +7 -0
  134. package/dist/lib/openapi/crud.js +137 -0
  135. package/dist/lib/openapi/crud.js.map +7 -0
  136. package/dist/lib/openapi/generator.js +1131 -0
  137. package/dist/lib/openapi/generator.js.map +7 -0
  138. package/dist/lib/openapi/index.js +10 -0
  139. package/dist/lib/openapi/index.js.map +7 -0
  140. package/dist/lib/openapi/sanitize.js +110 -0
  141. package/dist/lib/openapi/sanitize.js.map +7 -0
  142. package/dist/lib/openapi/types.js +1 -0
  143. package/dist/lib/openapi/types.js.map +7 -0
  144. package/dist/lib/profiler/index.js +258 -0
  145. package/dist/lib/profiler/index.js.map +7 -0
  146. package/dist/lib/query/engine.js +729 -0
  147. package/dist/lib/query/engine.js.map +7 -0
  148. package/dist/lib/query/join-utils.js +195 -0
  149. package/dist/lib/query/join-utils.js.map +7 -0
  150. package/dist/lib/query/types.js +9 -0
  151. package/dist/lib/query/types.js.map +7 -0
  152. package/dist/lib/search/config.js +32 -0
  153. package/dist/lib/search/config.js.map +7 -0
  154. package/dist/lib/search/tokenize.js +34 -0
  155. package/dist/lib/search/tokenize.js.map +7 -0
  156. package/dist/lib/slugify.js +24 -0
  157. package/dist/lib/slugify.js.map +7 -0
  158. package/dist/lib/testing/bootstrap.js +51 -0
  159. package/dist/lib/testing/bootstrap.js.map +7 -0
  160. package/dist/lib/testing/index.js +17 -0
  161. package/dist/lib/testing/index.js.map +7 -0
  162. package/dist/lib/testing/renderWithProviders.js +15 -0
  163. package/dist/lib/testing/renderWithProviders.js.map +7 -0
  164. package/dist/lib/url.js +12 -0
  165. package/dist/lib/url.js.map +7 -0
  166. package/dist/lib/utils.js +13 -0
  167. package/dist/lib/utils.js.map +7 -0
  168. package/dist/lib/version.js +7 -0
  169. package/dist/lib/version.js.map +7 -0
  170. package/dist/modules/dashboard/widgets.js +1 -0
  171. package/dist/modules/dashboard/widgets.js.map +7 -0
  172. package/dist/modules/dsl.js +30 -0
  173. package/dist/modules/dsl.js.map +7 -0
  174. package/dist/modules/entities/kinds.js +22 -0
  175. package/dist/modules/entities/kinds.js.map +7 -0
  176. package/dist/modules/entities/options.js +26 -0
  177. package/dist/modules/entities/options.js.map +7 -0
  178. package/dist/modules/entities/validation.js +102 -0
  179. package/dist/modules/entities/validation.js.map +7 -0
  180. package/dist/modules/entities/validators.js +88 -0
  181. package/dist/modules/entities/validators.js.map +7 -0
  182. package/dist/modules/entities.js +1 -0
  183. package/dist/modules/entities.js.map +7 -0
  184. package/dist/modules/navigation/sidebarPreferences.js +50 -0
  185. package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
  186. package/dist/modules/perspectives/types.js +1 -0
  187. package/dist/modules/perspectives/types.js.map +7 -0
  188. package/dist/modules/registry.js +96 -0
  189. package/dist/modules/registry.js.map +7 -0
  190. package/dist/modules/search.js +15 -0
  191. package/dist/modules/search.js.map +7 -0
  192. package/dist/modules/vector.js +1 -0
  193. package/dist/modules/vector.js.map +7 -0
  194. package/dist/modules/widgets/injection-loader.js +180 -0
  195. package/dist/modules/widgets/injection-loader.js.map +7 -0
  196. package/dist/modules/widgets/injection.js +1 -0
  197. package/dist/modules/widgets/injection.js.map +7 -0
  198. package/dist/security/features.js +23 -0
  199. package/dist/security/features.js.map +7 -0
  200. package/dist/types/pg.d.js +1 -0
  201. package/dist/types/pg.d.js.map +7 -0
  202. package/dist/types/react-email.d.js +1 -0
  203. package/dist/types/react-email.d.js.map +7 -0
  204. package/dist/types/resend.d.js +1 -0
  205. package/dist/types/resend.d.js.map +7 -0
  206. package/jest.config.cjs +22 -0
  207. package/package.json +88 -0
  208. package/src/index.ts +0 -0
  209. package/src/lib/api/__tests__/scoped.test.ts +38 -0
  210. package/src/lib/api/crud.ts +59 -0
  211. package/src/lib/api/scoped.ts +239 -0
  212. package/src/lib/auth/jwt.ts +39 -0
  213. package/src/lib/auth/server.ts +199 -0
  214. package/src/lib/boolean.ts +17 -0
  215. package/src/lib/bootstrap/appResolver.ts +85 -0
  216. package/src/lib/bootstrap/dynamicLoader.ts +177 -0
  217. package/src/lib/bootstrap/factory.ts +108 -0
  218. package/src/lib/bootstrap/index.ts +23 -0
  219. package/src/lib/bootstrap/types.ts +31 -0
  220. package/src/lib/cache/segments.ts +56 -0
  221. package/src/lib/cli/progress.ts +55 -0
  222. package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
  223. package/src/lib/commands/__tests__/helpers.test.ts +42 -0
  224. package/src/lib/commands/command-bus.ts +349 -0
  225. package/src/lib/commands/customFieldSnapshots.ts +86 -0
  226. package/src/lib/commands/helpers.ts +143 -0
  227. package/src/lib/commands/index.ts +4 -0
  228. package/src/lib/commands/operationMetadata.ts +40 -0
  229. package/src/lib/commands/registry.ts +46 -0
  230. package/src/lib/commands/scope.ts +59 -0
  231. package/src/lib/commands/types.ts +63 -0
  232. package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
  233. package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
  234. package/src/lib/crud/cache-stats.ts +127 -0
  235. package/src/lib/crud/cache.ts +205 -0
  236. package/src/lib/crud/custom-fields-client.ts +54 -0
  237. package/src/lib/crud/custom-fields.ts +607 -0
  238. package/src/lib/crud/errors.ts +23 -0
  239. package/src/lib/crud/exporters.ts +188 -0
  240. package/src/lib/crud/factory.ts +1622 -0
  241. package/src/lib/crud/types.ts +29 -0
  242. package/src/lib/custom-fields/normalize.ts +45 -0
  243. package/src/lib/data/engine.ts +562 -0
  244. package/src/lib/db/escapeLikePattern.ts +2 -0
  245. package/src/lib/db/mikro.ts +100 -0
  246. package/src/lib/di/container.ts +105 -0
  247. package/src/lib/email/send.ts +18 -0
  248. package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
  249. package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
  250. package/src/lib/encryption/aes.ts +64 -0
  251. package/src/lib/encryption/customFieldValues.ts +67 -0
  252. package/src/lib/encryption/entityFields.ts +39 -0
  253. package/src/lib/encryption/entityIds.ts +107 -0
  254. package/src/lib/encryption/find.ts +81 -0
  255. package/src/lib/encryption/indexDoc.ts +104 -0
  256. package/src/lib/encryption/kms.ts +337 -0
  257. package/src/lib/encryption/subscriber.ts +416 -0
  258. package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
  259. package/src/lib/encryption/toggles.ts +15 -0
  260. package/src/lib/entities/naming.ts +6 -0
  261. package/src/lib/entities/system-entities.ts +43 -0
  262. package/src/lib/frontend/organizationEvents.ts +55 -0
  263. package/src/lib/frontend/useOrganizationScope.ts +30 -0
  264. package/src/lib/hotkeys/index.ts +168 -0
  265. package/src/lib/i18n/app-dictionaries.ts +18 -0
  266. package/src/lib/i18n/config.ts +4 -0
  267. package/src/lib/i18n/context.tsx +66 -0
  268. package/src/lib/i18n/server.ts +74 -0
  269. package/src/lib/i18n/translate.ts +54 -0
  270. package/src/lib/indexers/error-log.ts +106 -0
  271. package/src/lib/indexers/status-log.ts +119 -0
  272. package/src/lib/lib/auth/jwt.ts +39 -0
  273. package/src/lib/lib/auth/server.ts +94 -0
  274. package/src/lib/lib/email/send.ts +18 -0
  275. package/src/lib/lib/i18n/config.ts +4 -0
  276. package/src/lib/lib/i18n/context.tsx +38 -0
  277. package/src/lib/lib/utils.ts +6 -0
  278. package/src/lib/location/countries.ts +97 -0
  279. package/src/lib/modules/index.ts +1 -0
  280. package/src/lib/modules/registry.ts +18 -0
  281. package/src/lib/openapi/crud.ts +218 -0
  282. package/src/lib/openapi/generator.ts +1311 -0
  283. package/src/lib/openapi/index.ts +4 -0
  284. package/src/lib/openapi/sanitize.ts +137 -0
  285. package/src/lib/openapi/types.ts +79 -0
  286. package/src/lib/profiler/index.ts +371 -0
  287. package/src/lib/query/__tests__/engine.test.ts +274 -0
  288. package/src/lib/query/engine.ts +837 -0
  289. package/src/lib/query/join-utils.ts +238 -0
  290. package/src/lib/query/types.ts +121 -0
  291. package/src/lib/search/config.ts +49 -0
  292. package/src/lib/search/tokenize.ts +45 -0
  293. package/src/lib/slugify.ts +28 -0
  294. package/src/lib/testing/bootstrap.ts +124 -0
  295. package/src/lib/testing/index.ts +15 -0
  296. package/src/lib/testing/renderWithProviders.tsx +31 -0
  297. package/src/lib/url.ts +12 -0
  298. package/src/lib/utils.ts +17 -0
  299. package/src/lib/version.ts +5 -0
  300. package/src/modules/__tests__/dsl.test.ts +35 -0
  301. package/src/modules/__tests__/registry.test.ts +300 -0
  302. package/src/modules/dashboard/widgets.ts +57 -0
  303. package/src/modules/dsl.ts +32 -0
  304. package/src/modules/entities/__tests__/validation.test.ts +52 -0
  305. package/src/modules/entities/kinds.ts +20 -0
  306. package/src/modules/entities/options.ts +36 -0
  307. package/src/modules/entities/validation.ts +118 -0
  308. package/src/modules/entities/validators.ts +93 -0
  309. package/src/modules/entities.ts +102 -0
  310. package/src/modules/navigation/sidebarPreferences.ts +62 -0
  311. package/src/modules/perspectives/types.ts +40 -0
  312. package/src/modules/registry.ts +249 -0
  313. package/src/modules/search.ts +325 -0
  314. package/src/modules/vector.ts +122 -0
  315. package/src/modules/widgets/__tests__/injection.test.ts +48 -0
  316. package/src/modules/widgets/injection-loader.ts +235 -0
  317. package/src/modules/widgets/injection.ts +120 -0
  318. package/src/security/features.ts +22 -0
  319. package/src/types/pg.d.ts +2 -0
  320. package/src/types/react-email.d.ts +2 -0
  321. package/src/types/resend.d.ts +2 -0
  322. package/tsconfig.build.json +11 -0
  323. package/tsconfig.json +9 -0
  324. package/watch.mjs +6 -0
@@ -0,0 +1,729 @@
1
+ import {
2
+ applyJoinFilters,
3
+ normalizeFilters,
4
+ partitionFilters,
5
+ resolveJoins
6
+ } from "./join-utils.js";
7
+ import { resolveSearchConfig } from "../search/config.js";
8
+ import { tokenizeText } from "../search/tokenize.js";
9
+ const entityTableCache = /* @__PURE__ */ new Map();
10
+ const pluralizeBaseName = (name) => {
11
+ if (!name) return name;
12
+ if (name.endsWith("s")) return name;
13
+ if (name.endsWith("y")) return `${name.slice(0, -1)}ies`;
14
+ return `${name}s`;
15
+ };
16
+ const toPascalCase = (value) => {
17
+ return value.split(/[_\s]+/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
18
+ };
19
+ const candidateClassNames = (rawName) => {
20
+ const base = toPascalCase(rawName);
21
+ const candidates = /* @__PURE__ */ new Set();
22
+ if (base) candidates.add(base);
23
+ if (base && !base.endsWith("Entity")) candidates.add(`${base}Entity`);
24
+ return Array.from(candidates);
25
+ };
26
+ function resolveEntityTableName(em, entity) {
27
+ if (entityTableCache.has(entity)) {
28
+ return entityTableCache.get(entity);
29
+ }
30
+ const parts = String(entity || "").split(":");
31
+ const rawName = parts[1] && parts[1].trim().length > 0 ? parts[1] : (parts[0] || "").trim();
32
+ const metadata = em?.getMetadata?.();
33
+ if (metadata && rawName) {
34
+ const candidates = candidateClassNames(rawName);
35
+ for (const candidate of candidates) {
36
+ try {
37
+ const meta = metadata.find?.(candidate);
38
+ if (meta?.tableName) {
39
+ const tableName = String(meta.tableName);
40
+ entityTableCache.set(entity, tableName);
41
+ return tableName;
42
+ }
43
+ } catch {
44
+ }
45
+ }
46
+ }
47
+ const fallback = pluralizeBaseName(rawName || "");
48
+ entityTableCache.set(entity, fallback);
49
+ return fallback;
50
+ }
51
+ class BasicQueryEngine {
52
+ constructor(em, getKnexFn, resolveEncryptionService) {
53
+ this.em = em;
54
+ this.getKnexFn = getKnexFn;
55
+ this.resolveEncryptionService = resolveEncryptionService;
56
+ this.columnCache = /* @__PURE__ */ new Map();
57
+ this.tableCache = /* @__PURE__ */ new Map();
58
+ this.searchAliasSeq = 0;
59
+ }
60
+ getEncryptionService() {
61
+ try {
62
+ return this.resolveEncryptionService?.() ?? null;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ async query(entity, opts = {}) {
68
+ const table = resolveEntityTableName(this.em, entity);
69
+ const knex = this.getKnexFn ? this.getKnexFn() : this.em.getConnection().getKnex();
70
+ let q = knex(table);
71
+ const qualify = (col) => `${table}.${col}`;
72
+ const orgScope = this.resolveOrganizationScope(opts);
73
+ this.searchAliasSeq = 0;
74
+ if (!opts.tenantId) {
75
+ throw new Error(
76
+ "QueryEngine: tenantId is now required for all queries (breaking change). Please provide a tenantId in QueryOptions, e.g., query(entity, { tenantId: ... }). See migration guide or documentation for details."
77
+ );
78
+ }
79
+ if (orgScope && await this.columnExists(table, "organization_id")) {
80
+ q = this.applyOrganizationScope(q, qualify("organization_id"), orgScope);
81
+ }
82
+ if (await this.columnExists(table, "tenant_id")) {
83
+ q = q.where(qualify("tenant_id"), opts.tenantId);
84
+ }
85
+ if (!opts.withDeleted && await this.columnExists(table, "deleted_at")) {
86
+ q = q.whereNull(qualify("deleted_at"));
87
+ }
88
+ const normalizedFilters = normalizeFilters(opts.filters);
89
+ const resolvedJoins = resolveJoins(table, opts.joins, (entityId) => resolveEntityTableName(this.em, entityId));
90
+ const joinMap = /* @__PURE__ */ new Map();
91
+ const aliasTables = /* @__PURE__ */ new Map();
92
+ aliasTables.set(table, table);
93
+ aliasTables.set("base", table);
94
+ for (const join of resolvedJoins) {
95
+ joinMap.set(join.alias, join);
96
+ aliasTables.set(join.alias, join.table);
97
+ }
98
+ const { baseFilters, joinFilters } = partitionFilters(table, normalizedFilters, joinMap);
99
+ const cfFilters = normalizedFilters.filter((filter) => String(filter.field).startsWith("cf:"));
100
+ const searchConfig = resolveSearchConfig();
101
+ const searchEnabled = searchConfig.enabled && await this.tableExists("search_tokens");
102
+ const hasSearchTokens = searchEnabled ? await this.hasSearchTokens(String(entity), opts.tenantId ?? null, orgScope) : false;
103
+ const searchActive = searchEnabled && hasSearchTokens;
104
+ const searchFilters = [...baseFilters, ...cfFilters].filter((filter) => filter.op === "like" || filter.op === "ilike");
105
+ if (searchFilters.length) {
106
+ const fields = searchFilters.map((filter) => String(filter.field));
107
+ this.logSearchDebug("search:init", {
108
+ entity: String(entity),
109
+ table,
110
+ tenantId: opts.tenantId ?? null,
111
+ organizationScope: orgScope,
112
+ fields,
113
+ searchEnabled,
114
+ hasSearchTokens,
115
+ searchActive,
116
+ searchConfig: {
117
+ enabled: searchConfig.enabled,
118
+ minTokenLength: searchConfig.minTokenLength,
119
+ enablePartials: searchConfig.enablePartials,
120
+ hashAlgorithm: searchConfig.hashAlgorithm,
121
+ blocklistedFields: searchConfig.blocklistedFields
122
+ }
123
+ });
124
+ if (!searchEnabled) {
125
+ this.logSearchDebug("search:disabled", { entity: String(entity), table });
126
+ } else if (!hasSearchTokens) {
127
+ this.logSearchDebug("search:no-search-tokens", {
128
+ entity: String(entity),
129
+ table,
130
+ tenantId: opts.tenantId ?? null,
131
+ organizationScope: orgScope
132
+ });
133
+ }
134
+ }
135
+ const recordIdColumn = qualify("id");
136
+ const applyFilterOp = (builder, column, op, value, fieldName) => {
137
+ if ((op === "like" || op === "ilike") && searchActive && typeof value === "string" && fieldName) {
138
+ const tokens = tokenizeText(String(value), searchConfig);
139
+ const hashes = tokens.hashes;
140
+ if (hashes.length) {
141
+ const applied = this.applySearchTokens(builder, {
142
+ entity: String(entity),
143
+ field: fieldName,
144
+ hashes,
145
+ recordIdColumn,
146
+ tenantId: opts.tenantId ?? null,
147
+ organizationScope: orgScope,
148
+ tokens: tokens.tokens
149
+ });
150
+ this.logSearchDebug("search:filter", {
151
+ entity: String(entity),
152
+ field: fieldName,
153
+ tokens: tokens.tokens,
154
+ hashes,
155
+ applied,
156
+ tenantId: opts.tenantId ?? null,
157
+ organizationScope: orgScope
158
+ });
159
+ if (applied) return builder;
160
+ } else {
161
+ this.logSearchDebug("search:skip-empty-hashes", {
162
+ entity: String(entity),
163
+ field: fieldName,
164
+ value
165
+ });
166
+ }
167
+ }
168
+ switch (op) {
169
+ case "eq":
170
+ builder.where(column, value);
171
+ break;
172
+ case "ne":
173
+ builder.whereNot(column, value);
174
+ break;
175
+ case "gt":
176
+ builder.where(column, ">", value);
177
+ break;
178
+ case "gte":
179
+ builder.where(column, ">=", value);
180
+ break;
181
+ case "lt":
182
+ builder.where(column, "<", value);
183
+ break;
184
+ case "lte":
185
+ builder.where(column, "<=", value);
186
+ break;
187
+ case "in":
188
+ builder.whereIn(column, Array.isArray(value) ? value : [value]);
189
+ break;
190
+ case "nin":
191
+ builder.whereNotIn(column, Array.isArray(value) ? value : [value]);
192
+ break;
193
+ case "like":
194
+ builder.where(column, "like", value);
195
+ break;
196
+ case "ilike":
197
+ builder.where(column, "ilike", value);
198
+ break;
199
+ case "exists":
200
+ value ? builder.whereNotNull(column) : builder.whereNull(column);
201
+ break;
202
+ default:
203
+ break;
204
+ }
205
+ return builder;
206
+ };
207
+ for (const filter of baseFilters) {
208
+ let qualified = filter.qualified ?? null;
209
+ if (!qualified) {
210
+ const column = await this.resolveBaseColumn(table, String(filter.field));
211
+ if (!column) continue;
212
+ qualified = qualify(column);
213
+ }
214
+ applyFilterOp(q, qualified, filter.op, filter.value, String(filter.field));
215
+ }
216
+ const applyAliasScopes = async (builder, aliasName) => {
217
+ const targetTable = aliasTables.get(aliasName);
218
+ if (!targetTable) return;
219
+ if (orgScope && await this.columnExists(targetTable, "organization_id")) {
220
+ this.applyOrganizationScope(builder, `${aliasName}.organization_id`, orgScope);
221
+ }
222
+ if (opts.tenantId && await this.columnExists(targetTable, "tenant_id")) {
223
+ builder.where(`${aliasName}.tenant_id`, opts.tenantId);
224
+ }
225
+ };
226
+ await applyJoinFilters({
227
+ knex,
228
+ baseTable: table,
229
+ builder: q,
230
+ joinMap,
231
+ joinFilters,
232
+ aliasTables,
233
+ qualifyBase: (column) => qualify(column),
234
+ applyAliasScope: (builder, alias) => applyAliasScopes(builder, alias),
235
+ applyFilterOp,
236
+ columnExists: (tbl, column) => this.columnExists(tbl, column)
237
+ });
238
+ if (opts.fields && opts.fields.length) {
239
+ const cols = opts.fields.filter((f) => !f.startsWith("cf:"));
240
+ if (cols.length) {
241
+ const baseSelects = cols.map((c) => knex.raw("?? as ??", [qualify(c), c]));
242
+ q = q.select(baseSelects);
243
+ }
244
+ } else {
245
+ q = q.select(knex.raw("??.*", [table]));
246
+ }
247
+ const tenantId = opts.tenantId;
248
+ const sanitize = (s) => s.replace(/[^a-zA-Z0-9_]/g, "_");
249
+ const cfSources = this.configureCustomFieldSources(q, table, entity, knex, opts, qualify);
250
+ const entityIdToSource = /* @__PURE__ */ new Map();
251
+ for (const source of cfSources) {
252
+ entityIdToSource.set(String(source.entityId), source);
253
+ }
254
+ const requestedCustomFieldKeys = Array.isArray(opts.includeCustomFields) ? opts.includeCustomFields.map((key) => String(key)) : [];
255
+ const cfKeys = /* @__PURE__ */ new Set();
256
+ const keySource = /* @__PURE__ */ new Map();
257
+ for (const f of opts.fields || []) {
258
+ if (typeof f === "string" && f.startsWith("cf:")) cfKeys.add(f.slice(3));
259
+ }
260
+ for (const f of cfFilters) {
261
+ if (typeof f.field === "string" && f.field.startsWith("cf:")) cfKeys.add(f.field.slice(3));
262
+ }
263
+ if (opts.includeCustomFields === true) {
264
+ if (entityIdToSource.size > 0) {
265
+ const entityIdList = Array.from(entityIdToSource.keys());
266
+ const entityOrder = /* @__PURE__ */ new Map();
267
+ entityIdList.forEach((id, idx) => entityOrder.set(id, idx));
268
+ const rows = await knex("custom_field_defs").select("key", "entity_id", "config_json", "kind").whereIn("entity_id", entityIdList).andWhere("is_active", true).modify((qb) => {
269
+ qb.andWhere((inner) => {
270
+ inner.where({ tenant_id: tenantId }).orWhereNull("tenant_id");
271
+ });
272
+ });
273
+ const sorted = rows.map((row) => {
274
+ const raw = row.config_json;
275
+ let cfg = {};
276
+ if (raw && typeof raw === "string") {
277
+ try {
278
+ cfg = JSON.parse(raw);
279
+ } catch {
280
+ cfg = {};
281
+ }
282
+ } else if (raw && typeof raw === "object") {
283
+ cfg = raw;
284
+ }
285
+ return {
286
+ key: String(row.key),
287
+ entityId: String(row.entity_id),
288
+ kind: String(row.kind || ""),
289
+ config: cfg
290
+ };
291
+ });
292
+ sorted.sort((a, b) => {
293
+ const ai = entityOrder.get(a.entityId) ?? Number.MAX_SAFE_INTEGER;
294
+ const bi = entityOrder.get(b.entityId) ?? Number.MAX_SAFE_INTEGER;
295
+ if (ai !== bi) return ai - bi;
296
+ return a.key.localeCompare(b.key);
297
+ });
298
+ const selectedSources = /* @__PURE__ */ new Map();
299
+ for (const row of sorted) {
300
+ const source = entityIdToSource.get(row.entityId);
301
+ if (!source) continue;
302
+ const cfg = row.config || {};
303
+ const entityIndex = entityOrder.get(row.entityId) ?? Number.MAX_SAFE_INTEGER;
304
+ const scores = computeScore(cfg, row.kind, entityIndex);
305
+ const existing = selectedSources.get(row.key);
306
+ if (!existing || scores.base > existing.score || scores.base === existing.score && (scores.penalty < existing.penalty || scores.penalty === existing.penalty && scores.entityIndex < existing.entityIndex)) {
307
+ selectedSources.set(row.key, { source, score: scores.base, penalty: scores.penalty, entityIndex: scores.entityIndex });
308
+ }
309
+ cfKeys.add(row.key);
310
+ }
311
+ for (const [key, entry] of selectedSources.entries()) {
312
+ keySource.set(key, entry.source);
313
+ }
314
+ }
315
+ } else if (requestedCustomFieldKeys.length > 0) {
316
+ for (const key of requestedCustomFieldKeys) cfKeys.add(key);
317
+ }
318
+ const unresolvedKeys = Array.from(cfKeys).filter((key) => !keySource.has(key));
319
+ if (unresolvedKeys.length > 0 && entityIdToSource.size > 0) {
320
+ const rows = await knex("custom_field_defs").select("key", "entity_id").whereIn("entity_id", Array.from(entityIdToSource.keys())).whereIn("key", unresolvedKeys).andWhere("is_active", true).modify((qb) => {
321
+ qb.andWhere((inner) => {
322
+ inner.where({ tenant_id: tenantId }).orWhereNull("tenant_id");
323
+ });
324
+ });
325
+ for (const row of rows) {
326
+ const source = entityIdToSource.get(String(row.entity_id));
327
+ if (!source) continue;
328
+ if (!keySource.has(row.key)) keySource.set(row.key, source);
329
+ }
330
+ }
331
+ const cfValueExprByKey = {};
332
+ const cfSelectedAliases = [];
333
+ const cfJsonAliases = /* @__PURE__ */ new Set();
334
+ const cfMultiAliasByAlias = /* @__PURE__ */ new Map();
335
+ for (const key of cfKeys) {
336
+ const source = keySource.get(key);
337
+ if (!source) continue;
338
+ const entityIdForKey = source.entityId;
339
+ const recordIdExpr = source.recordIdExpr;
340
+ const sourceAliasSafe = sanitize(source.alias || "src");
341
+ const keyAliasSafe = sanitize(key);
342
+ const defAlias = `cfd_${sourceAliasSafe}_${keyAliasSafe}`;
343
+ const valAlias = `cfv_${sourceAliasSafe}_${keyAliasSafe}`;
344
+ q = q.leftJoin({ [defAlias]: "custom_field_defs" }, function() {
345
+ this.on(`${defAlias}.entity_id`, "=", knex.raw("?", [entityIdForKey])).andOn(`${defAlias}.key`, "=", knex.raw("?", [key])).andOn(`${defAlias}.is_active`, "=", knex.raw("true")).andOn(knex.raw(`(${defAlias}.tenant_id = ? OR ${defAlias}.tenant_id IS NULL)`, [tenantId]));
346
+ });
347
+ q = q.leftJoin({ [valAlias]: "custom_field_values" }, function() {
348
+ this.on(`${valAlias}.entity_id`, "=", knex.raw("?", [entityIdForKey])).andOn(`${valAlias}.field_key`, "=", knex.raw("?", [key])).andOn(`${valAlias}.record_id`, "=", recordIdExpr).andOn(knex.raw(`(${valAlias}.tenant_id = ? OR ${valAlias}.tenant_id IS NULL)`, [tenantId]));
349
+ });
350
+ const caseExpr = knex.raw(
351
+ `CASE ${defAlias}.kind
352
+ WHEN 'integer' THEN (${valAlias}.value_int)::text
353
+ WHEN 'float' THEN (${valAlias}.value_float)::text
354
+ WHEN 'boolean' THEN (${valAlias}.value_bool)::text
355
+ WHEN 'multiline' THEN (${valAlias}.value_multiline)::text
356
+ ELSE (${valAlias}.value_text)::text
357
+ END`
358
+ );
359
+ cfValueExprByKey[key] = caseExpr;
360
+ const alias = sanitize(`cf:${key}`);
361
+ if ((opts.fields || []).includes(`cf:${key}`) || opts.includeCustomFields === true || requestedCustomFieldKeys.length > 0 && requestedCustomFieldKeys.includes(key)) {
362
+ const isMulti = knex.raw(`bool_or(coalesce((${defAlias}.config_json->>'multi')::boolean, false))`);
363
+ const aggregatedArray = `array_remove(array_agg(DISTINCT ${caseExpr.toString()}), NULL)`;
364
+ const expr = `CASE WHEN ${isMulti.toString()}
365
+ THEN to_jsonb(${aggregatedArray})
366
+ ELSE to_jsonb(max(${caseExpr.toString()}))
367
+ END`;
368
+ const multiAlias = `${alias}__is_multi`;
369
+ q = q.select(knex.raw(`${expr} as ??`, [alias]));
370
+ q = q.select(knex.raw(`${isMulti.toString()} as ??`, [multiAlias]));
371
+ cfSelectedAliases.push(alias);
372
+ cfJsonAliases.add(alias);
373
+ cfMultiAliasByAlias.set(alias, multiAlias);
374
+ }
375
+ }
376
+ for (const f of cfFilters) {
377
+ if (!f.field.startsWith("cf:")) continue;
378
+ const key = f.field.slice(3);
379
+ const expr = cfValueExprByKey[key];
380
+ if (!expr) continue;
381
+ if ((f.op === "like" || f.op === "ilike") && searchActive && typeof f.value === "string") {
382
+ const tokens = tokenizeText(String(f.value), searchConfig);
383
+ const hashes = tokens.hashes;
384
+ if (hashes.length) {
385
+ const applied = this.applySearchTokens(q, {
386
+ entity: String(entity),
387
+ field: f.field,
388
+ hashes,
389
+ recordIdColumn,
390
+ tenantId: opts.tenantId ?? null,
391
+ organizationScope: orgScope,
392
+ tokens: tokens.tokens
393
+ });
394
+ this.logSearchDebug("search:cf-filter", {
395
+ entity: String(entity),
396
+ field: f.field,
397
+ tokens: tokens.tokens,
398
+ hashes,
399
+ applied,
400
+ tenantId: opts.tenantId ?? null,
401
+ organizationScope: orgScope
402
+ });
403
+ if (applied) continue;
404
+ } else {
405
+ this.logSearchDebug("search:cf-skip-empty-hashes", {
406
+ entity: String(entity),
407
+ field: f.field,
408
+ value: f.value
409
+ });
410
+ }
411
+ }
412
+ switch (f.op) {
413
+ case "eq":
414
+ q = q.where(expr, "=", f.value);
415
+ break;
416
+ case "ne":
417
+ q = q.where(expr, "!=", f.value);
418
+ break;
419
+ case "gt":
420
+ q = q.where(expr, ">", f.value);
421
+ break;
422
+ case "gte":
423
+ q = q.where(expr, ">=", f.value);
424
+ break;
425
+ case "lt":
426
+ q = q.where(expr, "<", f.value);
427
+ break;
428
+ case "lte":
429
+ q = q.where(expr, "<=", f.value);
430
+ break;
431
+ case "in":
432
+ q = q.whereIn(expr, f.value ?? []);
433
+ break;
434
+ case "nin":
435
+ q = q.whereNotIn(expr, f.value ?? []);
436
+ break;
437
+ case "like":
438
+ q = q.where(expr, "like", f.value);
439
+ break;
440
+ case "ilike":
441
+ q = q.where(expr, "ilike", f.value);
442
+ break;
443
+ case "exists":
444
+ f.value ? q = q.whereNotNull(expr) : q = q.whereNull(expr);
445
+ break;
446
+ }
447
+ }
448
+ if (opts.includeExtensions) {
449
+ const { getModules } = await import("@open-mercato/shared/lib/i18n/server");
450
+ const allMods = getModules();
451
+ const allExts = allMods.flatMap((m) => m.entityExtensions || []);
452
+ const exts = allExts.filter((e) => e.base === entity);
453
+ const chosen = Array.isArray(opts.includeExtensions) ? exts.filter((e) => opts.includeExtensions.includes(e.extension)) : exts;
454
+ for (const e of chosen) {
455
+ const [, extName] = e.extension.split(":");
456
+ const extTable = extName.endsWith("s") ? extName : `${extName}s`;
457
+ const alias = `ext_${sanitize(extName)}`;
458
+ q = q.leftJoin({ [alias]: extTable }, function() {
459
+ this.on(`${alias}.${e.join.extensionKey}`, "=", knex.raw("??", [`${table}.${e.join.baseKey}`]));
460
+ });
461
+ }
462
+ }
463
+ for (const s of opts.sort || []) {
464
+ if (s.field.startsWith("cf:")) {
465
+ const key = s.field.slice(3);
466
+ const alias = sanitize(`cf:${key}`);
467
+ if (!cfSelectedAliases.includes(alias)) {
468
+ const expr = cfValueExprByKey[key];
469
+ if (expr) {
470
+ q = q.select(knex.raw(`max(${expr.toString()}) as ??`, [alias]));
471
+ cfSelectedAliases.push(alias);
472
+ }
473
+ }
474
+ q = q.orderBy(alias, s.dir ?? "asc");
475
+ } else {
476
+ const column = await this.resolveBaseColumn(table, s.field);
477
+ if (!column) continue;
478
+ q = q.orderBy(qualify(column), s.dir ?? "asc");
479
+ }
480
+ }
481
+ const page = opts.page?.page ?? 1;
482
+ const pageSize = opts.page?.pageSize ?? 20;
483
+ if (opts.includeExtensions && (Array.isArray(opts.includeExtensions) ? opts.includeExtensions.length > 0 : true) || Object.keys(cfValueExprByKey).length > 0) {
484
+ q = q.groupBy(`${table}.id`);
485
+ }
486
+ const countClone = q.clone();
487
+ if (typeof countClone.clearSelect === "function") countClone.clearSelect();
488
+ if (typeof countClone.clearOrder === "function") countClone.clearOrder();
489
+ if (typeof countClone.clearGroup === "function") countClone.clearGroup();
490
+ const countRow = await countClone.countDistinct(`${table}.id as count`).first();
491
+ const total = Number(countRow?.count ?? 0);
492
+ const items = await q.limit(pageSize).offset((page - 1) * pageSize);
493
+ if (cfJsonAliases.size > 0) {
494
+ for (const row of items) {
495
+ for (const alias of cfJsonAliases) {
496
+ const multiAlias = cfMultiAliasByAlias.get(alias);
497
+ const isMulti = multiAlias ? Boolean(row[multiAlias]) : false;
498
+ let raw = row[alias];
499
+ if (typeof raw === "string") {
500
+ try {
501
+ raw = JSON.parse(raw);
502
+ } catch {
503
+ }
504
+ }
505
+ if (isMulti) {
506
+ if (raw == null) row[alias] = [];
507
+ else if (Array.isArray(raw)) row[alias] = raw;
508
+ else row[alias] = [raw];
509
+ } else {
510
+ if (Array.isArray(raw)) row[alias] = raw.length > 0 ? raw[0] : null;
511
+ else row[alias] = raw;
512
+ }
513
+ if (multiAlias) delete row[multiAlias];
514
+ }
515
+ }
516
+ }
517
+ const svc = this.getEncryptionService();
518
+ const decryptPayload = svc?.decryptEntityPayload?.bind(svc);
519
+ let decryptedItems = items;
520
+ if (decryptPayload) {
521
+ const fallbackOrgId = opts.organizationId ?? (Array.isArray(opts.organizationIds) && opts.organizationIds.length === 1 ? opts.organizationIds[0] : null);
522
+ decryptedItems = await Promise.all(
523
+ items.map(async (item) => {
524
+ try {
525
+ const decrypted = await decryptPayload(
526
+ entity,
527
+ item,
528
+ item?.tenant_id ?? item?.tenantId ?? opts.tenantId ?? null,
529
+ item?.organization_id ?? item?.organizationId ?? fallbackOrgId ?? null
530
+ );
531
+ return { ...item, ...decrypted };
532
+ } catch (err) {
533
+ console.error("QueryEngine: error decrypting entity payload", err);
534
+ return item;
535
+ }
536
+ })
537
+ );
538
+ }
539
+ return { items: decryptedItems, page, pageSize, total };
540
+ }
541
+ async resolveBaseColumn(table, field) {
542
+ if (await this.columnExists(table, field)) return field;
543
+ if (field === "organization_id" && await this.columnExists(table, "id")) return "id";
544
+ return null;
545
+ }
546
+ async columnExists(table, column) {
547
+ const key = `${table}.${column}`;
548
+ if (this.columnCache.has(key)) {
549
+ const cached = this.columnCache.get(key);
550
+ if (cached === true) return true;
551
+ this.columnCache.delete(key);
552
+ }
553
+ const knex = this.getKnexFn ? this.getKnexFn() : this.em.getConnection().getKnex();
554
+ const exists = await knex("information_schema.columns").where({ table_name: table, column_name: column }).first();
555
+ const present = !!exists;
556
+ if (present) this.columnCache.set(key, true);
557
+ else this.columnCache.delete(key);
558
+ return present;
559
+ }
560
+ async tableExists(table) {
561
+ if (this.tableCache.has(table)) return this.tableCache.get(table) ?? false;
562
+ const knex = this.getKnexFn ? this.getKnexFn() : this.em.getConnection().getKnex();
563
+ const exists = await knex("information_schema.tables").where({ table_name: table }).first();
564
+ const present = !!exists;
565
+ this.tableCache.set(table, present);
566
+ return present;
567
+ }
568
+ async hasSearchTokens(entity, tenantId, orgScope) {
569
+ try {
570
+ const knex = this.getKnexFn ? this.getKnexFn() : this.em.getConnection().getKnex();
571
+ const query = knex("search_tokens").select(1).where("entity_type", entity).limit(1);
572
+ if (tenantId !== void 0) {
573
+ query.andWhereRaw("tenant_id is not distinct from ?", [tenantId]);
574
+ }
575
+ if (orgScope) {
576
+ this.applyOrganizationScope(query, "search_tokens.organization_id", orgScope);
577
+ }
578
+ const row = await query.first();
579
+ return !!row;
580
+ } catch (err) {
581
+ this.logSearchDebug("search:has-tokens-error", {
582
+ entity,
583
+ tenantId,
584
+ organizationScope: orgScope,
585
+ error: err instanceof Error ? err.message : String(err)
586
+ });
587
+ return false;
588
+ }
589
+ }
590
+ applySearchTokens(q, opts) {
591
+ if (!opts.hashes.length) {
592
+ this.logSearchDebug("search:skip-no-hashes", {
593
+ entity: opts.entity,
594
+ field: opts.field,
595
+ tenantId: opts.tenantId ?? null,
596
+ organizationScope: opts.organizationScope
597
+ });
598
+ return false;
599
+ }
600
+ const alias = `st_${this.searchAliasSeq++}`;
601
+ const combineWith = opts.combineWith === "or" ? "orWhereExists" : "whereExists";
602
+ const engine = this;
603
+ this.logSearchDebug("search:apply-search-tokens", {
604
+ entity: opts.entity,
605
+ field: opts.field,
606
+ alias,
607
+ tokenCount: opts.hashes.length,
608
+ tokens: opts.tokens,
609
+ tenantId: opts.tenantId ?? null,
610
+ organizationScope: opts.organizationScope,
611
+ combineWith: opts.combineWith ?? "and"
612
+ });
613
+ q[combineWith](function() {
614
+ 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]);
615
+ if (opts.tenantId !== void 0) {
616
+ this.andWhereRaw(`${alias}.tenant_id is not distinct from ?`, [opts.tenantId ?? null]);
617
+ }
618
+ if (opts.organizationScope) {
619
+ engine.applyOrganizationScope(this, `${alias}.organization_id`, opts.organizationScope);
620
+ }
621
+ });
622
+ return true;
623
+ }
624
+ configureCustomFieldSources(q, baseTable, baseEntity, knex, opts, qualify) {
625
+ const sources = [
626
+ {
627
+ entityId: baseEntity,
628
+ alias: "base",
629
+ table: baseTable,
630
+ recordIdExpr: knex.raw("??::text", [`${baseTable}.id`])
631
+ }
632
+ ];
633
+ const extras = opts.customFieldSources ?? [];
634
+ extras.forEach((srcOpt, index) => {
635
+ const joinTable = srcOpt.table ?? resolveEntityTableName(this.em, srcOpt.entityId);
636
+ const alias = srcOpt.alias ?? `cfs_${index}`;
637
+ const join = srcOpt.join;
638
+ if (!join) {
639
+ throw new Error(`QueryEngine: customFieldSources entry for ${String(srcOpt.entityId)} requires a join configuration`);
640
+ }
641
+ const joinArgs = { [alias]: joinTable };
642
+ const joinCallback = function() {
643
+ this.on(`${alias}.${join.toField}`, "=", qualify(join.fromField));
644
+ };
645
+ const joinType = join.type ?? "left";
646
+ if (joinType === "inner") q.join(joinArgs, joinCallback);
647
+ else q.leftJoin(joinArgs, joinCallback);
648
+ const recordColumn = srcOpt.recordIdColumn ?? "id";
649
+ sources.push({
650
+ entityId: srcOpt.entityId,
651
+ alias,
652
+ table: joinTable,
653
+ recordIdExpr: knex.raw("??::text", [`${alias}.${recordColumn}`])
654
+ });
655
+ });
656
+ return sources;
657
+ }
658
+ logSearchDebug(event, payload) {
659
+ try {
660
+ console.info("[query:search]", event, JSON.stringify(payload));
661
+ } catch {
662
+ console.info("[query:search]", event, payload);
663
+ }
664
+ }
665
+ resolveOrganizationScope(opts) {
666
+ if (opts.organizationIds !== void 0) {
667
+ const raw = (opts.organizationIds ?? []).map((id) => typeof id === "string" ? id.trim() : id);
668
+ const includeNull = raw.some((id) => id == null || id === "");
669
+ const ids = raw.filter((id) => typeof id === "string" && id.length > 0);
670
+ return { ids: Array.from(new Set(ids)), includeNull };
671
+ }
672
+ if (typeof opts.organizationId === "string" && opts.organizationId.trim().length > 0) {
673
+ return { ids: [opts.organizationId], includeNull: false };
674
+ }
675
+ return null;
676
+ }
677
+ applyOrganizationScope(q, column, scope) {
678
+ if (!scope) return q;
679
+ if (scope.ids.length === 0 && !scope.includeNull) {
680
+ return q.whereRaw("1 = 0");
681
+ }
682
+ return q.where((builder) => {
683
+ let applied = false;
684
+ if (scope.ids.length > 0) {
685
+ builder.whereIn(column, scope.ids);
686
+ applied = true;
687
+ }
688
+ if (scope.includeNull) {
689
+ if (applied) builder.orWhereNull(column);
690
+ else builder.whereNull(column);
691
+ applied = true;
692
+ }
693
+ if (!applied) builder.whereRaw("1 = 0");
694
+ });
695
+ }
696
+ }
697
+ const computeScore = (cfg, kind, entityIndex) => {
698
+ const listVisibleScore = cfg.listVisible === false ? 0 : 1;
699
+ const formEditableScore = cfg.formEditable === false ? 0 : 1;
700
+ const filterableScore = cfg.filterable ? 1 : 0;
701
+ const kindScore = (() => {
702
+ switch (kind) {
703
+ case "dictionary":
704
+ return 8;
705
+ case "relation":
706
+ return 6;
707
+ case "select":
708
+ return 4;
709
+ case "multiline":
710
+ return 3;
711
+ case "boolean":
712
+ case "integer":
713
+ case "float":
714
+ return 2;
715
+ default:
716
+ return 1;
717
+ }
718
+ })();
719
+ const optionsBonus = Array.isArray(cfg.options) && cfg.options.length ? 2 : 0;
720
+ const dictionaryBonus = typeof cfg.dictionaryId === "string" && cfg.dictionaryId.trim().length ? 5 : 0;
721
+ const base = listVisibleScore * 16 + formEditableScore * 8 + filterableScore * 4 + kindScore + optionsBonus + dictionaryBonus;
722
+ const penalty = typeof cfg.priority === "number" ? cfg.priority : 0;
723
+ return { base, penalty, entityIndex };
724
+ };
725
+ export {
726
+ BasicQueryEngine,
727
+ resolveEntityTableName
728
+ };
729
+ //# sourceMappingURL=engine.js.map