@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,238 @@
1
+ import type { Knex } from 'knex'
2
+ import type { QueryOptions, QueryJoinEdge } from './types'
3
+ import type { FilterOp } from './types'
4
+
5
+ export type NormalizedFilter = { field: string; op: FilterOp; value?: unknown }
6
+
7
+ export function normalizeFilters(filters?: QueryOptions['filters']): NormalizedFilter[] {
8
+ if (!filters) return []
9
+ const normalizeField = (key: string) => (key.startsWith('cf_') ? `cf:${key.slice(3)}` : key)
10
+ if (Array.isArray(filters)) {
11
+ return (filters as any[]).map((f) => ({
12
+ ...f,
13
+ field: normalizeField(String((f as any).field)),
14
+ }))
15
+ }
16
+ const out: NormalizedFilter[] = []
17
+ const obj = filters as Record<string, unknown>
18
+ const push = (field: string, op: FilterOp, value?: unknown) => {
19
+ out.push({ field, op, value })
20
+ }
21
+ for (const [rawKey, rawVal] of Object.entries(obj)) {
22
+ const field = normalizeField(rawKey)
23
+ if (rawVal !== null && typeof rawVal === 'object' && !Array.isArray(rawVal)) {
24
+ for (const [opKey, opVal] of Object.entries(rawVal as Record<string, unknown>)) {
25
+ switch (opKey) {
26
+ case '$eq':
27
+ push(field, 'eq', opVal)
28
+ break
29
+ case '$ne':
30
+ push(field, 'ne', opVal)
31
+ break
32
+ case '$gt':
33
+ push(field, 'gt', opVal)
34
+ break
35
+ case '$gte':
36
+ push(field, 'gte', opVal)
37
+ break
38
+ case '$lt':
39
+ push(field, 'lt', opVal)
40
+ break
41
+ case '$lte':
42
+ push(field, 'lte', opVal)
43
+ break
44
+ case '$in':
45
+ push(field, 'in', opVal)
46
+ break
47
+ case '$nin':
48
+ push(field, 'nin', opVal)
49
+ break
50
+ case '$like':
51
+ push(field, 'like', opVal)
52
+ break
53
+ case '$ilike':
54
+ push(field, 'ilike', opVal)
55
+ break
56
+ case '$exists':
57
+ push(field, 'exists', opVal)
58
+ break
59
+ }
60
+ }
61
+ } else {
62
+ push(field, 'eq', rawVal)
63
+ }
64
+ }
65
+ return out
66
+ }
67
+
68
+ export type ResolvedJoin = {
69
+ alias: string
70
+ table: string
71
+ fromAlias: string
72
+ fromField: string
73
+ toField: string
74
+ type: 'left' | 'inner'
75
+ }
76
+
77
+ export type BaseFilter = NormalizedFilter & { qualified?: string }
78
+ export type JoinFilter = { alias: string; column: string; op: FilterOp; value?: unknown }
79
+
80
+ export function resolveJoins(
81
+ baseTable: string,
82
+ joins: QueryJoinEdge[] | null | undefined,
83
+ resolveTable: (entityId: string) => string | null,
84
+ ): ResolvedJoin[] {
85
+ if (!joins || joins.length === 0) return []
86
+ const resolved: ResolvedJoin[] = []
87
+ const seen = new Set<string>()
88
+ for (const entry of joins) {
89
+ if (!entry || typeof entry !== 'object') continue
90
+ const alias = typeof entry.alias === 'string' ? entry.alias.trim() : ''
91
+ if (!alias) continue
92
+ if (seen.has(alias)) continue
93
+ const table =
94
+ entry.table ??
95
+ (entry.entityId ? resolveTable(String(entry.entityId)) : null)
96
+ if (!table) continue
97
+ const fromField = entry.from?.field?.trim()
98
+ const toField = entry.to?.field?.trim()
99
+ if (!fromField || !toField) continue
100
+ const fromAliasRaw = entry.from?.alias?.trim()
101
+ const fromAlias = fromAliasRaw && fromAliasRaw.length > 0 ? fromAliasRaw : 'base'
102
+ const type: 'left' | 'inner' = entry.type === 'inner' ? 'inner' : 'left'
103
+ resolved.push({ alias, table, fromAlias, fromField, toField, type })
104
+ seen.add(alias)
105
+ }
106
+ return resolved
107
+ }
108
+
109
+ export function buildJoinChain(
110
+ alias: string,
111
+ joinMap: Map<string, ResolvedJoin>,
112
+ baseTable: string,
113
+ visited: Set<string> = new Set(),
114
+ ): ResolvedJoin[] {
115
+ if (visited.has(alias)) {
116
+ throw new Error(`QueryEngine: circular join reference detected for alias ${alias}`)
117
+ }
118
+ const cfg = joinMap.get(alias)
119
+ if (!cfg) return []
120
+ visited.add(alias)
121
+ if (!cfg.fromAlias || cfg.fromAlias === 'base' || cfg.fromAlias === baseTable) {
122
+ return [cfg]
123
+ }
124
+ const parentChain = buildJoinChain(cfg.fromAlias, joinMap, baseTable, visited)
125
+ if (parentChain.length === 0) return []
126
+ return [...parentChain, cfg]
127
+ }
128
+
129
+ export function partitionFilters(
130
+ baseTable: string,
131
+ filters: NormalizedFilter[],
132
+ joinMap: Map<string, ResolvedJoin>,
133
+ ): { baseFilters: BaseFilter[]; joinFilters: Map<string, JoinFilter[]> } {
134
+ const baseFilters: BaseFilter[] = []
135
+ const joinFilters = new Map<string, JoinFilter[]>()
136
+ for (const filter of filters) {
137
+ const field = String(filter.field)
138
+ if (field.startsWith('cf:')) continue
139
+ const parts = field.split('.')
140
+ if (parts.length === 2) {
141
+ const [aliasNameRaw, column] = parts
142
+ const aliasName = aliasNameRaw || ''
143
+ if (joinMap.has(aliasName)) {
144
+ const list = joinFilters.get(aliasName) ?? []
145
+ list.push({ alias: aliasName, column, op: filter.op, value: filter.value })
146
+ joinFilters.set(aliasName, list)
147
+ continue
148
+ }
149
+ if (aliasName === baseTable || aliasName === 'base') {
150
+ baseFilters.push({
151
+ field: column,
152
+ op: filter.op,
153
+ value: filter.value,
154
+ qualified: `${baseTable}.${column}`,
155
+ })
156
+ continue
157
+ }
158
+ }
159
+ baseFilters.push({ ...filter })
160
+ }
161
+ return { baseFilters, joinFilters }
162
+ }
163
+
164
+ type ApplyJoinFiltersOptions = {
165
+ knex: Knex
166
+ baseTable: string
167
+ builder: Knex.QueryBuilder
168
+ joinMap: Map<string, ResolvedJoin>
169
+ joinFilters: Map<string, JoinFilter[]>
170
+ aliasTables: Map<string, string>
171
+ qualifyBase: (column: string) => string
172
+ applyAliasScope: (builder: Knex.QueryBuilder, alias: string, table: string) => Promise<void> | void
173
+ applyFilterOp: (builder: Knex.QueryBuilder, column: string, op: FilterOp, value?: unknown) => void
174
+ columnExists?: (table: string, column: string) => Promise<boolean> | boolean
175
+ }
176
+
177
+ export async function applyJoinFilters({
178
+ knex,
179
+ baseTable,
180
+ builder,
181
+ joinMap,
182
+ joinFilters,
183
+ aliasTables,
184
+ qualifyBase,
185
+ applyAliasScope,
186
+ applyFilterOp,
187
+ columnExists,
188
+ }: ApplyJoinFiltersOptions): Promise<Knex.QueryBuilder> {
189
+ const resolveAliasName = (aliasName?: string | null) => {
190
+ if (!aliasName || aliasName === 'base') return baseTable
191
+ return aliasName
192
+ }
193
+
194
+ for (const [alias, filtersForAlias] of joinFilters.entries()) {
195
+ const chain = buildJoinChain(alias, joinMap, baseTable)
196
+ if (!chain.length) continue
197
+ const first = chain[0]
198
+ const sub = knex({ [first.alias]: first.table }).select(1)
199
+ await applyAliasScope(sub, first.alias, first.table)
200
+ const parentAlias = resolveAliasName(first.fromAlias)
201
+ if (parentAlias === baseTable) {
202
+ sub.whereRaw('?? = ??', [`${first.alias}.${first.toField}`, qualifyBase(first.fromField)])
203
+ } else {
204
+ sub.whereRaw('?? = ??', [`${first.alias}.${first.toField}`, `${parentAlias}.${first.fromField}`])
205
+ }
206
+ for (const cfg of chain.slice(1)) {
207
+ const joinArgs = { [cfg.alias]: cfg.table }
208
+ const parent = resolveAliasName(cfg.fromAlias)
209
+ const joinFn = function (this: Knex.JoinClause) {
210
+ const left = `${cfg.alias}.${cfg.toField}`
211
+ const right = parent === baseTable ? qualifyBase(cfg.fromField) : `${parent}.${cfg.fromField}`
212
+ this.on(knex.raw('?? = ??', [left, right]))
213
+ }
214
+ if (cfg.type === 'inner') sub.join(joinArgs, joinFn)
215
+ else sub.leftJoin(joinArgs, joinFn)
216
+ await applyAliasScope(sub, cfg.alias, cfg.table)
217
+ }
218
+ let existsDirective: boolean | null = null
219
+ for (const filter of filtersForAlias) {
220
+ if (filter.op === 'exists') {
221
+ if (filter.value === false) existsDirective = false
222
+ else if (existsDirective === null) existsDirective = true
223
+ continue
224
+ }
225
+ const targetTable = aliasTables.get(filter.alias)
226
+ if (!targetTable) continue
227
+ if (columnExists) {
228
+ const exists = await columnExists(targetTable, filter.column)
229
+ if (!exists) continue
230
+ }
231
+ const qualified = `${filter.alias}.${filter.column}`
232
+ applyFilterOp(sub, qualified, filter.op, filter.value)
233
+ }
234
+ if (existsDirective === false) builder = builder.whereNotExists(sub)
235
+ else builder = builder.whereExists(sub)
236
+ }
237
+ return builder
238
+ }
@@ -0,0 +1,121 @@
1
+ import type { EntityId } from '@open-mercato/shared/modules/entities'
2
+ import type { Profiler } from '../profiler'
3
+
4
+ export type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'like' | 'ilike' | 'exists'
5
+
6
+ export enum SortDir {
7
+ Asc = 'asc',
8
+ Desc = 'desc',
9
+ }
10
+
11
+ export type FieldSelector = string // base field or custom field key (prefixed with 'cf:')
12
+
13
+ export type Filter = {
14
+ field: FieldSelector
15
+ op: FilterOp
16
+ value?: any
17
+ }
18
+
19
+ export type Sort = { field: FieldSelector; dir?: SortDir }
20
+
21
+ export type Page = { page?: number; pageSize?: number }
22
+
23
+ // Mongo/Medusa-style filter operators (typed)
24
+ export type WhereOps<T> = {
25
+ $eq?: T
26
+ $ne?: T | null
27
+ $gt?: T extends number | Date ? T : never
28
+ $gte?: T extends number | Date ? T : never
29
+ $lt?: T extends number | Date ? T : never
30
+ $lte?: T extends number | Date ? T : never
31
+ $in?: T[]
32
+ $nin?: T[]
33
+ $like?: T extends string ? string : never
34
+ $ilike?: T extends string ? string : never
35
+ $exists?: boolean
36
+ }
37
+
38
+ // A field filter can be a direct value (equals) or ops object
39
+ export type WhereValue<T = any> = T | WhereOps<T>
40
+
41
+ // Generic shape for object filters. If you have a typed map of field→type,
42
+ // pass it as the generic to get end-to-end typing.
43
+ // Example: Where<{
44
+ // id: string; title: string; created_at: Date; 'cf:severity': number
45
+ // }>
46
+ export type Where<Fields extends Record<string, any> = Record<string, any>> =
47
+ Partial<{ [K in keyof Fields]: WhereValue<Fields[K]> }> & Record<string, WhereValue>
48
+
49
+ export type QueryCustomFieldJoin = {
50
+ fromField: string
51
+ toField: string
52
+ type?: 'left' | 'inner'
53
+ }
54
+
55
+ export type QueryCustomFieldSource = {
56
+ entityId: EntityId
57
+ table?: string
58
+ alias?: string
59
+ recordIdColumn?: string
60
+ join?: QueryCustomFieldJoin
61
+ tenantField?: string
62
+ organizationField?: string
63
+ }
64
+
65
+ export type QueryJoinEdge = {
66
+ alias: string
67
+ table?: string
68
+ entityId?: EntityId
69
+ from: {
70
+ alias?: string
71
+ field: string
72
+ }
73
+ to: {
74
+ field: string
75
+ }
76
+ type?: 'left' | 'inner'
77
+ }
78
+
79
+ export type QueryOptions = {
80
+ fields?: FieldSelector[] // base fields and/or 'cf:<key>' for custom fields
81
+ includeExtensions?: boolean | string[] // include all registered extensions or only specific ones by entity id
82
+ includeCustomFields?: boolean | string[] // include all CFs or specific keys
83
+ // Accept classic array syntax or Mongo-style object syntax
84
+ filters?: Filter[] | Where
85
+ sort?: Sort[]
86
+ page?: Page
87
+ organizationId?: string // enforce multi-tenant scope
88
+ tenantId?: string // enforce tenant scope
89
+ // Optional list of organization ids to scope results. Takes precedence over organizationId.
90
+ organizationIds?: string[]
91
+ // Soft-delete behavior: when false (default), rows with non-null deleted_at
92
+ // are excluded if the base table has that column. Set true to include them.
93
+ withDeleted?: boolean
94
+ customFieldSources?: QueryCustomFieldSource[]
95
+ joins?: QueryJoinEdge[]
96
+ profiler?: Profiler
97
+ }
98
+
99
+ export type PartialIndexWarning = {
100
+ entity: EntityId
101
+ entityLabel?: string | null
102
+ baseCount?: number | null
103
+ indexedCount?: number | null
104
+ scope?: 'scoped' | 'global'
105
+ }
106
+
107
+ export type QueryResultMeta = {
108
+ partialIndexWarning?: PartialIndexWarning
109
+ }
110
+
111
+ export type QueryResult<T = any> = {
112
+ items: T[]
113
+ page: number
114
+ pageSize: number
115
+ total: number
116
+ meta?: QueryResultMeta
117
+ }
118
+
119
+ export interface QueryEngine {
120
+ query<T = any>(entity: EntityId, opts?: QueryOptions): Promise<QueryResult<T>>
121
+ }
@@ -0,0 +1,49 @@
1
+ import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
2
+
3
+ export type SearchConfig = {
4
+ enabled: boolean
5
+ minTokenLength: number
6
+ enablePartials: boolean
7
+ hashAlgorithm: 'sha256' | 'sha1' | 'md5'
8
+ storeRawTokens: boolean
9
+ blocklistedFields: string[]
10
+ }
11
+
12
+ const DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']
13
+
14
+ function parseBoolean(raw: string | undefined, fallback: boolean): boolean {
15
+ return parseBooleanWithDefault(raw, fallback)
16
+ }
17
+
18
+ function parseNumber(raw: string | undefined, fallback: number, min = 1): number {
19
+ if (raw == null) return fallback
20
+ const value = Number.parseInt(raw, 10)
21
+ if (!Number.isFinite(value)) return fallback
22
+ if (value < min) return fallback
23
+ return value
24
+ }
25
+
26
+ function parseHashAlgorithm(raw: string | undefined): 'sha256' | 'sha1' | 'md5' {
27
+ const value = (raw ?? '').trim().toLowerCase()
28
+ if (value === 'sha1') return 'sha1'
29
+ if (value === 'md5') return 'md5'
30
+ return 'sha256'
31
+ }
32
+
33
+ export function resolveSearchConfig(): SearchConfig {
34
+ return {
35
+ enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),
36
+ minTokenLength: parseNumber(process.env.OM_SEARCH_MIN_LEN, 3, 1),
37
+ enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),
38
+ hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),
39
+ storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),
40
+ blocklistedFields: (process.env.OM_SEARCH_FIELD_BLOCKLIST ?? '')
41
+ .split(',')
42
+ .map((entry) => entry.trim())
43
+ .filter((entry) => entry.length > 0)
44
+ .filter((value, index, arr) => arr.indexOf(value) === index)
45
+ .map((entry) => entry.toLowerCase())
46
+ .concat(DEFAULT_BLOCKLIST)
47
+ .filter((value, index, arr) => arr.indexOf(value) === index),
48
+ }
49
+ }
@@ -0,0 +1,45 @@
1
+ import crypto from 'crypto'
2
+ import { resolveSearchConfig, type SearchConfig } from './config'
3
+
4
+ export type TokenizationResult = {
5
+ tokens: string[]
6
+ hashes: string[]
7
+ }
8
+
9
+ function normalizeText(text: string): string {
10
+ return text
11
+ .normalize('NFKD')
12
+ .replace(/[\u0300-\u036f]/g, '')
13
+ .replace(/[%_]/g, ' ')
14
+ .toLowerCase()
15
+ }
16
+
17
+ function splitTokens(text: string, minLength: number): string[] {
18
+ return normalizeText(text)
19
+ .split(/[^a-z0-9]+/i)
20
+ .filter((token) => token.length >= minLength)
21
+ }
22
+
23
+ function expandToken(token: string, config: SearchConfig): string[] {
24
+ if (!config.enablePartials) return [token]
25
+ const results: string[] = []
26
+ for (let i = config.minTokenLength; i <= token.length; i += 1) {
27
+ results.push(token.slice(0, i))
28
+ }
29
+ return results
30
+ }
31
+
32
+ export function hashToken(token: string, config?: SearchConfig): string {
33
+ const cfg = config ?? resolveSearchConfig()
34
+ return crypto.createHash(cfg.hashAlgorithm).update(token).digest('hex')
35
+ }
36
+
37
+ export function tokenizeText(text: string, config?: SearchConfig): TokenizationResult {
38
+ const cfg = config ?? resolveSearchConfig()
39
+ const baseTokens = splitTokens(text, cfg.minTokenLength)
40
+ const expanded = baseTokens.flatMap((token) => expandToken(token, cfg))
41
+ const unique = Array.from(new Set(expanded))
42
+ const tokens = unique.filter((token) => token.length >= cfg.minTokenLength)
43
+ const hashes = tokens.map((token) => hashToken(token, cfg))
44
+ return { tokens, hashes }
45
+ }
@@ -0,0 +1,28 @@
1
+ export type SlugifyOptions = {
2
+ replacement?: string
3
+ allowedChars?: string
4
+ trimReplacement?: boolean
5
+ }
6
+
7
+ const DEFAULT_REPLACEMENT = '-'
8
+ const DEFAULT_ALLOWED_CHARS = '-'
9
+
10
+ const escapeRegex = (value: string): string => value.replace(/[\\^$.*+?()[\]{}|\-]/g, '\\$&')
11
+
12
+ export function slugify(value: string, options: SlugifyOptions = {}): string {
13
+ const replacement = options.replacement ?? DEFAULT_REPLACEMENT
14
+ const allowedChars = options.allowedChars ?? DEFAULT_ALLOWED_CHARS
15
+ const trimReplacement = options.trimReplacement ?? true
16
+ const normalized = value.toLowerCase().trim()
17
+ if (!normalized) return ''
18
+ const escapedAllowed = escapeRegex(allowedChars)
19
+ const invalidPattern = new RegExp(`[^a-z0-9${escapedAllowed}]+`, 'g')
20
+ const replaced = normalized.replace(invalidPattern, replacement)
21
+ if (!trimReplacement || replacement.length !== 1 || !replaced) return replaced
22
+ const char = replacement
23
+ let start = 0
24
+ let end = replaced.length
25
+ while (start < end && replaced[start] === char) start += 1
26
+ while (end > start && replaced[end - 1] === char) end -= 1
27
+ return replaced.slice(start, end)
28
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Test Bootstrap Utility
3
+ *
4
+ * Provides a centralized way to bootstrap dependencies for tests.
5
+ * This utility allows tests to register only the dependencies they need
6
+ * without importing the full app bootstrap.
7
+ *
8
+ * Usage in tests:
9
+ *
10
+ * ```typescript
11
+ * import { bootstrapTest, resetTestBootstrap } from '@open-mercato/shared/lib/testing/bootstrap'
12
+ *
13
+ * beforeEach(async () => {
14
+ * resetTestBootstrap()
15
+ * await bootstrapTest({
16
+ * modules: mockModules,
17
+ * entityIds: mockEntityIds,
18
+ * })
19
+ * })
20
+ * ```
21
+ *
22
+ * For tests that use jest.resetModules(), import dynamically:
23
+ *
24
+ * ```typescript
25
+ * beforeEach(async () => {
26
+ * jest.resetModules()
27
+ * const { registerModules } = await import('@open-mercato/shared/modules/registry')
28
+ * registerModules(mockModules as any)
29
+ * })
30
+ * ```
31
+ */
32
+
33
+ import type { Module } from '../../modules/registry'
34
+
35
+ export type EntityIds = Record<string, Record<string, string>>
36
+
37
+ export interface TestBootstrapOptions {
38
+ /** Modules to register (for i18n, query engine, etc.) */
39
+ modules?: Module[]
40
+ /** Entity IDs to register (for encryption, indexing) */
41
+ entityIds?: EntityIds
42
+ /** ORM entities to register (rarely needed in unit tests) */
43
+ ormEntities?: any[]
44
+ /** DI registrars to register (rarely needed in unit tests) */
45
+ diRegistrars?: Array<(container: any) => void>
46
+ }
47
+
48
+ let _testBootstrapped = false
49
+
50
+ /**
51
+ * Bootstrap dependencies for tests.
52
+ * Call this in beforeEach or beforeAll to set up required registrations.
53
+ */
54
+ export async function bootstrapTest(options: TestBootstrapOptions = {}): Promise<void> {
55
+ const { modules, entityIds, ormEntities, diRegistrars } = options
56
+
57
+ if (modules !== undefined) {
58
+ // Import lazily to avoid circular dependencies
59
+ const { registerModules } = await import('../modules/registry.js')
60
+ registerModules(modules)
61
+ }
62
+
63
+ if (entityIds !== undefined) {
64
+ const { registerEntityIds } = await import('../encryption/entityIds.js')
65
+ registerEntityIds(entityIds)
66
+ }
67
+
68
+ if (ormEntities !== undefined) {
69
+ const { registerOrmEntities } = await import('../db/mikro.js')
70
+ registerOrmEntities(ormEntities)
71
+ }
72
+
73
+ if (diRegistrars !== undefined) {
74
+ const { registerDiRegistrars } = await import('../di/container.js')
75
+ registerDiRegistrars(diRegistrars)
76
+ }
77
+
78
+ _testBootstrapped = true
79
+ }
80
+
81
+ /**
82
+ * Reset the test bootstrap state.
83
+ * Call this in beforeEach when you need fresh state between tests.
84
+ *
85
+ * Note: This only resets the test bootstrap flag. To fully reset
86
+ * registration state, you may need to use jest.resetModules() and
87
+ * re-import the registration functions.
88
+ */
89
+ export function resetTestBootstrap(): void {
90
+ _testBootstrapped = false
91
+ }
92
+
93
+ /**
94
+ * Check if test bootstrap has been called.
95
+ */
96
+ export function isTestBootstrapped(): boolean {
97
+ return _testBootstrapped
98
+ }
99
+
100
+ /**
101
+ * Helper to create minimal mock modules for testing.
102
+ */
103
+ export function createMockModules(overrides: Partial<Module>[] = []): Module[] {
104
+ return overrides.map((override, index) => ({
105
+ id: override.id || `test-module-${index}`,
106
+ ...override,
107
+ })) as Module[]
108
+ }
109
+
110
+ /**
111
+ * Helper to create minimal mock entity IDs for testing.
112
+ */
113
+ export function createMockEntityIds(
114
+ entities: Record<string, string[]>
115
+ ): EntityIds {
116
+ const result: EntityIds = {}
117
+ for (const [module, entityNames] of Object.entries(entities)) {
118
+ result[module] = {}
119
+ for (const name of entityNames) {
120
+ result[module][name] = `${module}:${name}`
121
+ }
122
+ }
123
+ return result
124
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Testing utilities for @open-mercato packages
3
+ */
4
+
5
+ export {
6
+ bootstrapTest,
7
+ resetTestBootstrap,
8
+ isTestBootstrapped,
9
+ createMockModules,
10
+ createMockEntityIds,
11
+ type TestBootstrapOptions,
12
+ type EntityIds,
13
+ } from './bootstrap'
14
+
15
+ export { renderWithProviders } from './renderWithProviders'
@@ -0,0 +1,31 @@
1
+ import * as React from 'react'
2
+ import type { RenderOptions } from '@testing-library/react'
3
+ import { render } from '@testing-library/react'
4
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5
+ import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
6
+
7
+ type ProviderOptions = {
8
+ locale?: string
9
+ dict?: Record<string, unknown>
10
+ queryClient?: QueryClient
11
+ }
12
+
13
+ export function renderWithProviders(
14
+ ui: React.ReactElement,
15
+ options?: RenderOptions & ProviderOptions,
16
+ ) {
17
+ const { locale = 'en', dict = {}, queryClient = new QueryClient(), ...rest } = options ?? {}
18
+
19
+ function Wrapper({ children }: { children: React.ReactNode }) {
20
+ return (
21
+ <QueryClientProvider client={queryClient}>
22
+ {/* @ts-expect-error shared provider accepts loose dict shape */}
23
+ <I18nProvider locale={locale} dict={dict}>
24
+ {children}
25
+ </I18nProvider>
26
+ </QueryClientProvider>
27
+ )
28
+ }
29
+
30
+ return render(ui, { wrapper: Wrapper, ...rest })
31
+ }
package/src/lib/url.ts ADDED
@@ -0,0 +1,12 @@
1
+ export function getAppBaseUrl(req: Request): string {
2
+ const url = new URL(req.url)
3
+ return (
4
+ process.env.NEXT_PUBLIC_APP_URL ||
5
+ process.env.APP_URL ||
6
+ `${url.protocol}//${url.host}`
7
+ )
8
+ }
9
+
10
+ export function toAbsoluteUrl(req: Request, path: string): string {
11
+ return new URL(path, getAppBaseUrl(req)).toString()
12
+ }
@@ -0,0 +1,17 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
7
+
8
+ export function slugifyTagLabel(label: string): string {
9
+ return (
10
+ label
11
+ .toLowerCase()
12
+ .trim()
13
+ .replace(/[^a-z0-9]+/g, "-")
14
+ .replace(/^-+|-+$/g, "")
15
+ .slice(0, 80) || `tag-${Math.random().toString(36).slice(2, 10)}`
16
+ )
17
+ }
@@ -0,0 +1,5 @@
1
+ // Application version - replaced at build time by esbuild plugin
2
+ // The actual version is injected during the build process in build.mjs
3
+ // This source file uses a placeholder that gets replaced in dist/
4
+ export const APP_VERSION = '0.0.0-dev'
5
+ export const appVersion = APP_VERSION