@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,100 @@
1
+ import 'dotenv/config'
2
+ import 'reflect-metadata'
3
+ import { MikroORM } from '@mikro-orm/core'
4
+ import { PostgreSqlDriver } from '@mikro-orm/postgresql'
5
+
6
+ let ormInstance: MikroORM<PostgreSqlDriver> | null = null
7
+
8
+ // Registration pattern for publishable packages
9
+ let _entities: any[] | null = null
10
+
11
+ export function registerOrmEntities(entities: any[]) {
12
+ if (_entities !== null && process.env.NODE_ENV === 'development') {
13
+ console.debug('[Bootstrap] ORM entities re-registered (this may occur during HMR)')
14
+ }
15
+ _entities = entities
16
+ }
17
+
18
+ export function getOrmEntities(): any[] {
19
+ if (!_entities) {
20
+ throw new Error('[Bootstrap] ORM entities not registered. Call registerOrmEntities() at bootstrap.')
21
+ }
22
+ return _entities
23
+ }
24
+
25
+ export async function getOrm() {
26
+ if (ormInstance) {
27
+ return ormInstance
28
+ }
29
+ const entities = getOrmEntities()
30
+ const clientUrl = process.env.DATABASE_URL
31
+ if (!clientUrl) throw new Error('DATABASE_URL is not set')
32
+
33
+ // Parse connection pool settings from environment
34
+ const poolMin = parseInt(process.env.DB_POOL_MIN || '2')
35
+ const poolMax = parseInt(process.env.DB_POOL_MAX || '50')
36
+ const poolIdleTimeout = parseInt(process.env.DB_POOL_IDLE_TIMEOUT || '3000')
37
+ const poolAcquireTimeout = parseInt(process.env.DB_POOL_ACQUIRE_TIMEOUT || '6000')
38
+ const idleSessionTimeoutEnv = parseInt(process.env.DB_IDLE_SESSION_TIMEOUT_MS || '')
39
+ const idleInTxTimeoutEnv = parseInt(process.env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || '')
40
+ const idleSessionTimeoutMs = Number.isFinite(idleSessionTimeoutEnv)
41
+ ? idleSessionTimeoutEnv
42
+ : process.env.NODE_ENV === 'production'
43
+ ? undefined
44
+ : 600_000
45
+ const idleInTransactionTimeoutMs = Number.isFinite(idleInTxTimeoutEnv)
46
+ ? idleInTxTimeoutEnv
47
+ : process.env.NODE_ENV === 'production'
48
+ ? undefined
49
+ : 120_000
50
+ const connectionOptions =
51
+ idleSessionTimeoutMs && idleSessionTimeoutMs > 0
52
+ ? `-c idle_session_timeout=${idleSessionTimeoutMs}`
53
+ : undefined
54
+
55
+ ormInstance = await MikroORM.init<PostgreSqlDriver>({
56
+ driver: PostgreSqlDriver,
57
+ clientUrl,
58
+ entities,
59
+ debug: false,
60
+ // Connection pooling configuration
61
+ pool: {
62
+ min: poolMin,
63
+ max: poolMax,
64
+ idleTimeoutMillis: poolIdleTimeout,
65
+ acquireTimeoutMillis: poolAcquireTimeout,
66
+ // Close idle connections after 30 seconds
67
+ destroyTimeoutMillis: process.env.NODE_ENV === 'production' ? 30000 : 3000,
68
+ },
69
+ // Connection options
70
+ driverOptions: {
71
+ // Enable connection pooling
72
+ connection: {
73
+ // Maximum number of connections in the pool
74
+ max: poolMax,
75
+ // Minimum number of connections in the pool
76
+ min: poolMin,
77
+ // Close connections after this many milliseconds of inactivity
78
+ idleTimeoutMillis: poolIdleTimeout,
79
+ // Maximum time to wait for a connection from the pool
80
+ acquireTimeoutMillis: poolAcquireTimeout,
81
+ idle_in_transaction_session_timeout: idleInTransactionTimeoutMs,
82
+ options: connectionOptions,
83
+ },
84
+ },
85
+ })
86
+ return ormInstance
87
+ }
88
+
89
+
90
+ async function closeOrmIfLoaded(): Promise<void> {
91
+ if (ormInstance) {
92
+ await ormInstance.close(true)
93
+ ormInstance = null
94
+ }
95
+ }
96
+
97
+ // In dev mode, handle reloads cleanly without leaving dangling connections.
98
+ if (process.env.NODE_ENV !== 'production') {
99
+ void closeOrmIfLoaded()
100
+ }
@@ -0,0 +1,105 @@
1
+ import { createContainer, asValue, AwilixContainer, InjectionMode } from 'awilix'
2
+ import { RequestContext } from '@mikro-orm/core'
3
+ import { getOrm } from '@open-mercato/shared/lib/db/mikro'
4
+ import { EntityManager } from '@mikro-orm/postgresql'
5
+ import { BasicQueryEngine } from '@open-mercato/shared/lib/query/engine'
6
+ import { DefaultDataEngine } from '@open-mercato/shared/lib/data/engine'
7
+ import { commandRegistry, CommandBus } from '@open-mercato/shared/lib/commands'
8
+
9
+ export type AppContainer = AwilixContainer
10
+ export type DiRegistrar = (container: AwilixContainer) => void
11
+
12
+ // Registration pattern for publishable packages
13
+ // Use globalThis to survive tsx/esbuild module duplication issue where the same
14
+ // file can be loaded as multiple module instances when mixing dynamic and static imports
15
+ const GLOBAL_KEY = '__openMercatoDiRegistrars__'
16
+
17
+ function getGlobalRegistrars(): DiRegistrar[] | null {
18
+ return (globalThis as any)[GLOBAL_KEY] ?? null
19
+ }
20
+
21
+ function setGlobalRegistrars(registrars: DiRegistrar[]): void {
22
+ (globalThis as any)[GLOBAL_KEY] = registrars
23
+ }
24
+
25
+ export function registerDiRegistrars(registrars: DiRegistrar[]) {
26
+ const existing = getGlobalRegistrars()
27
+ if (existing !== null && process.env.NODE_ENV === 'development') {
28
+ console.debug('[Bootstrap] DI registrars re-registered (this may occur during HMR)')
29
+ }
30
+ setGlobalRegistrars(registrars)
31
+ }
32
+
33
+ export function getDiRegistrars(): DiRegistrar[] {
34
+ const registrars = getGlobalRegistrars()
35
+ if (!registrars) {
36
+ throw new Error('[Bootstrap] DI registrars not registered. Call registerDiRegistrars() at bootstrap.')
37
+ }
38
+ return registrars
39
+ }
40
+
41
+ export async function createRequestContainer(): Promise<AppContainer> {
42
+ const diRegistrars = getDiRegistrars()
43
+ const orm = await getOrm()
44
+ // Use a fresh event manager so request-level subscribers (e.g., encryption) don't pile up globally
45
+ const baseEm = (RequestContext.getEntityManager() as any) ?? orm.em
46
+ const em = baseEm.fork({ clear: true, freshEventManager: true, useContext: true }) as unknown as EntityManager
47
+ const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
48
+ // Core registrations
49
+ container.register({
50
+ em: asValue(em),
51
+ queryEngine: asValue(new BasicQueryEngine(em, undefined, () => {
52
+ try { return container.resolve('tenantEncryptionService') as any } catch { return null }
53
+ })),
54
+ dataEngine: asValue(new DefaultDataEngine(em, container as any)),
55
+ commandRegistry: asValue(commandRegistry),
56
+ commandBus: asValue(new CommandBus()),
57
+ })
58
+ // Allow modules to override/extend
59
+ for (const reg of diRegistrars) {
60
+ try { reg?.(container) } catch {}
61
+ }
62
+ // Core bootstrap (cache, event bus, encryption subscriber/KMS, module subscribers)
63
+ try {
64
+ const { bootstrap } = await import('@open-mercato/core/bootstrap') as any
65
+ if (bootstrap && typeof bootstrap === 'function') {
66
+ // Avoid double bootstrap if caller already wired it
67
+ const alreadyBootstrapped = !!container.registrations?.eventBus
68
+ if (!alreadyBootstrapped) {
69
+ await bootstrap(container)
70
+ }
71
+ }
72
+ } catch { /* optional */ }
73
+ // App-level DI override (last chance)
74
+ // This import path resolves only in the app context, not in packages
75
+ try {
76
+ // @ts-ignore - @/di only exists in app context, not in packages
77
+ const appDi = await import('@/di') as any
78
+ if (appDi?.register) {
79
+ try {
80
+ const maybe = appDi.register(container)
81
+ if (maybe && typeof maybe.then === 'function') await maybe
82
+ } catch {}
83
+ }
84
+ } catch {}
85
+ // Ensure tenant encryption subscriber is always registered on the fresh request-scoped EM
86
+ try {
87
+ const emForEnc = container.resolve('em') as any
88
+ const tenantEncryptionService = container.hasRegistration('tenantEncryptionService')
89
+ ? (container.resolve('tenantEncryptionService') as any)
90
+ : null
91
+ if (emForEnc && tenantEncryptionService?.isEnabled?.()) {
92
+ const { registerTenantEncryptionSubscriber } = await import('@open-mercato/shared/lib/encryption/subscriber')
93
+ registerTenantEncryptionSubscriber(emForEnc, tenantEncryptionService)
94
+ }
95
+ } catch {
96
+ // best-effort; do not block container creation
97
+ }
98
+ return container
99
+ }
100
+ try {
101
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
102
+ require('server-only')
103
+ } catch {
104
+ // allow CLI/generator usage where Next server-only is not present
105
+ }
@@ -0,0 +1,18 @@
1
+ import { Resend } from 'resend'
2
+ import React from 'react'
3
+
4
+ export type SendEmailOptions = {
5
+ to: string
6
+ subject: string
7
+ react: React.ReactElement
8
+ from?: string
9
+ }
10
+
11
+ export async function sendEmail({ to, subject, react, from }: SendEmailOptions) {
12
+ const apiKey = process.env.RESEND_API_KEY
13
+ if (!apiKey) throw new Error('RESEND_API_KEY is not set')
14
+ const resend = new Resend(apiKey)
15
+ const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'
16
+ await resend.emails.send({ to, subject, from: fromAddr, react })
17
+ }
18
+
@@ -0,0 +1,63 @@
1
+ import type { EntityManager } from '@mikro-orm/core'
2
+ import { decryptCustomFieldValue, encryptCustomFieldValue, resolveTenantEncryptionService } from '../customFieldValues'
3
+
4
+ const fixedKey = Buffer.alloc(32, 1).toString('base64')
5
+
6
+ describe('customFieldValues encryption helpers', () => {
7
+ it('caches tenant encryption service per entity manager', () => {
8
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
9
+ const em = {} as EntityManager
10
+ const first = resolveTenantEncryptionService(em)
11
+ const second = resolveTenantEncryptionService(em)
12
+ expect(first).toBe(second)
13
+ warnSpy.mockRestore()
14
+ })
15
+
16
+ it('encrypts and decrypts primitives when enabled', async () => {
17
+ const service = {
18
+ isEnabled: () => true,
19
+ getDek: async () => ({ key: fixedKey }),
20
+ } as any
21
+ const cache = new Map<string | null, string | null>()
22
+
23
+ const encrypted = await encryptCustomFieldValue('secret', 'tenant-1', service, cache)
24
+ expect(typeof encrypted).toBe('string')
25
+ const decrypted = await decryptCustomFieldValue(encrypted, 'tenant-1', service, cache)
26
+ expect(decrypted).toBe('secret')
27
+
28
+ const encryptedNumber = await encryptCustomFieldValue(42, 'tenant-1', service, cache)
29
+ const decryptedNumber = await decryptCustomFieldValue(encryptedNumber, 'tenant-1', service, cache)
30
+ expect(decryptedNumber).toBe(42)
31
+ })
32
+
33
+ it('creates a tenant DEK on first encrypt when none exists', async () => {
34
+ let created = false
35
+ const service = {
36
+ isEnabled: () => true,
37
+ getDek: jest.fn(async () => (created ? { key: fixedKey } : null)),
38
+ createDek: jest.fn(async () => {
39
+ created = true
40
+ return { key: fixedKey }
41
+ }),
42
+ } as any
43
+ const cache = new Map<string | null, string | null>()
44
+
45
+ const encrypted = await encryptCustomFieldValue('secret', 'tenant-1', service, cache)
46
+ expect(typeof encrypted).toBe('string')
47
+ expect(encrypted).not.toBe('secret')
48
+ expect(service.createDek).toHaveBeenCalledTimes(1)
49
+
50
+ const decrypted = await decryptCustomFieldValue(encrypted, 'tenant-1', service, cache)
51
+ expect(decrypted).toBe('secret')
52
+ })
53
+
54
+ it('returns original value when encryption is disabled or tenant is missing', async () => {
55
+ const disabledService = {
56
+ isEnabled: () => false,
57
+ getDek: async () => ({ key: fixedKey }),
58
+ } as any
59
+ expect(await encryptCustomFieldValue('plain', 'tenant-1', disabledService)).toBe('plain')
60
+ expect(await decryptCustomFieldValue('plain', 'tenant-1', disabledService)).toBe('plain')
61
+ expect(await encryptCustomFieldValue('plain', null, disabledService)).toBe('plain')
62
+ })
63
+ })
@@ -0,0 +1,115 @@
1
+ import { decryptIndexDocCustomFields, decryptIndexDocForSearch, encryptIndexDocForStorage } from '../indexDoc'
2
+ import { decryptCustomFieldValue } from '../customFieldValues'
3
+
4
+ jest.mock('../customFieldValues', () => ({
5
+ decryptCustomFieldValue: jest.fn(async (value: unknown) => value),
6
+ }))
7
+
8
+ const decryptCustomFieldValueMock = decryptCustomFieldValue as jest.Mock
9
+
10
+ describe('encryption/indexDoc', () => {
11
+ beforeEach(() => {
12
+ decryptCustomFieldValueMock.mockReset()
13
+ decryptCustomFieldValueMock.mockImplementation(async (value: unknown) => value)
14
+ })
15
+
16
+ test('decryptIndexDocCustomFields decrypts cf keys (including arrays)', async () => {
17
+ decryptCustomFieldValueMock.mockImplementation(async (value: unknown) => {
18
+ if (value === 'enc') return 'dec'
19
+ if (value === 'enc2') return 'dec2'
20
+ return value
21
+ })
22
+
23
+ const doc = {
24
+ id: '1',
25
+ title: 'Encrypted',
26
+ 'cf:secret': 'enc',
27
+ 'cf:tags': ['enc2', 'plain'],
28
+ cf_secret: 'enc',
29
+ }
30
+
31
+ const out = await decryptIndexDocCustomFields(doc, { tenantId: 't1', organizationId: 'org1' }, {} as any)
32
+ expect(out).toEqual({
33
+ id: '1',
34
+ title: 'Encrypted',
35
+ 'cf:secret': 'dec',
36
+ 'cf:tags': ['dec2', 'plain'],
37
+ cf_secret: 'dec',
38
+ })
39
+ expect(decryptCustomFieldValueMock).toHaveBeenCalled()
40
+ })
41
+
42
+ test('decryptIndexDocForSearch merges decrypted entity payload and decrypts cf keys', async () => {
43
+ decryptCustomFieldValueMock.mockImplementation(async (value: unknown) => (value === 'enc' ? 'dec' : value))
44
+
45
+ const service = {
46
+ isEnabled: () => true,
47
+ decryptEntityPayload: jest.fn(async (_entityId: string, _payload: Record<string, unknown>) => ({
48
+ title: 'Plain',
49
+ })),
50
+ }
51
+
52
+ const out = await decryptIndexDocForSearch(
53
+ 'example:todo',
54
+ { id: '1', title: 'Encrypted', 'cf:secret': 'enc' },
55
+ { tenantId: 't1', organizationId: 'org1' },
56
+ service as any,
57
+ )
58
+
59
+ expect(out.title).toBe('Plain')
60
+ expect(out['cf:secret']).toBe('dec')
61
+ expect(service.decryptEntityPayload).toHaveBeenCalledWith(
62
+ 'example:todo',
63
+ expect.any(Object),
64
+ 't1',
65
+ 'org1',
66
+ )
67
+ })
68
+
69
+ test('decryptIndexDocForSearch decrypts customer entity when indexing customer profiles', async () => {
70
+ const service = {
71
+ isEnabled: () => true,
72
+ decryptEntityPayload: jest.fn(async (entityId: string) => (entityId === 'customers:customer_entity' ? { display_name: 'Plain' } : {})),
73
+ }
74
+
75
+ const out = await decryptIndexDocForSearch(
76
+ 'customers:customer_person_profile',
77
+ { id: '1', display_name: 'Encrypted' },
78
+ { tenantId: 't1', organizationId: 'org1' },
79
+ service as any,
80
+ )
81
+
82
+ expect(out.display_name).toBe('Plain')
83
+ expect(service.decryptEntityPayload).toHaveBeenCalledWith(
84
+ 'customers:customer_entity',
85
+ expect.any(Object),
86
+ 't1',
87
+ 'org1',
88
+ )
89
+ })
90
+
91
+ test('encryptIndexDocForStorage encrypts entity fields using the configured map', async () => {
92
+ const service = {
93
+ isEnabled: () => true,
94
+ encryptEntityPayload: jest.fn(async (_entityId: string, payload: Record<string, unknown>) => ({
95
+ ...payload,
96
+ resultTitle: 'enc',
97
+ })),
98
+ }
99
+
100
+ const out = await encryptIndexDocForStorage(
101
+ 'vector:vector_search',
102
+ { resultTitle: 'plain' },
103
+ { tenantId: 't1', organizationId: 'org1' },
104
+ service as any,
105
+ )
106
+
107
+ expect(out.resultTitle).toBe('enc')
108
+ expect(service.encryptEntityPayload).toHaveBeenCalledWith(
109
+ 'vector:vector_search',
110
+ expect.any(Object),
111
+ 't1',
112
+ 'org1',
113
+ )
114
+ })
115
+ })
@@ -0,0 +1,64 @@
1
+ import crypto from 'node:crypto'
2
+ import { isEncryptionDebugEnabled } from './toggles'
3
+
4
+ export type EncryptionPayload = {
5
+ value: string | null
6
+ raw: string
7
+ version: string
8
+ }
9
+
10
+ export function generateDek(): string {
11
+ return crypto.randomBytes(32).toString('base64')
12
+ }
13
+
14
+ function logDebug(event: string, payload: Record<string, unknown>) {
15
+ if (!isEncryptionDebugEnabled()) return
16
+ try {
17
+ // eslint-disable-next-line no-console
18
+ console.debug('[encryption]', event, payload)
19
+ } catch {
20
+ // ignore
21
+ }
22
+ }
23
+
24
+ export function encryptWithAesGcm(value: string, dekBase64: string): EncryptionPayload {
25
+ const dek = Buffer.from(dekBase64, 'base64')
26
+ const iv = crypto.randomBytes(12)
27
+ const cipher = crypto.createCipheriv('aes-256-gcm', dek, iv)
28
+ const ciphertext = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()])
29
+ const tag = cipher.getAuthTag()
30
+ const payload = [
31
+ iv.toString('base64'),
32
+ ciphertext.toString('base64'),
33
+ tag.toString('base64'),
34
+ 'v1',
35
+ ].join(':')
36
+ logDebug('encrypt', { length: ciphertext.length })
37
+ return { value: payload, raw: payload, version: 'v1' }
38
+ }
39
+
40
+ export function decryptWithAesGcm(payload: string, dekBase64: string): string | null {
41
+ if (!payload) return null
42
+ const parts = payload.split(':')
43
+ if (parts.length !== 4) return null
44
+ const [ivB64, ciphertextB64, tagB64, version] = parts
45
+ if (version !== 'v1') return null
46
+ const dek = Buffer.from(dekBase64, 'base64')
47
+ const iv = Buffer.from(ivB64, 'base64')
48
+ const ciphertext = Buffer.from(ciphertextB64, 'base64')
49
+ const tag = Buffer.from(tagB64, 'base64')
50
+ try {
51
+ const decipher = crypto.createDecipheriv('aes-256-gcm', dek, iv)
52
+ decipher.setAuthTag(tag)
53
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')
54
+ logDebug('decrypt', { iv: ivB64, tag: tagB64 })
55
+ return decrypted
56
+ } catch (err) {
57
+ logDebug('decrypt_error', { message: (err as Error)?.message || String(err) })
58
+ return null
59
+ }
60
+ }
61
+
62
+ export function hashForLookup(value: string): string {
63
+ return crypto.createHash('sha256').update(value.toLowerCase().trim()).digest('hex')
64
+ }
@@ -0,0 +1,67 @@
1
+ import type { EntityManager } from '@mikro-orm/core'
2
+ import { encryptWithAesGcm, decryptWithAesGcm } from './aes'
3
+ import { TenantDataEncryptionService } from './tenantDataEncryptionService'
4
+
5
+ const serviceCache = new WeakMap<EntityManager, TenantDataEncryptionService>()
6
+
7
+ export function resolveTenantEncryptionService(
8
+ em: EntityManager,
9
+ provided?: TenantDataEncryptionService | null,
10
+ ): TenantDataEncryptionService | null {
11
+ if (provided) return provided
12
+ const cached = serviceCache.get(em)
13
+ if (cached) return cached
14
+ const service = new TenantDataEncryptionService(em as any)
15
+ serviceCache.set(em, service)
16
+ return service
17
+ }
18
+
19
+ async function resolveDekKey(
20
+ service: TenantDataEncryptionService | null,
21
+ tenantId: string | null | undefined,
22
+ cache?: Map<string | null, string | null>,
23
+ opts?: { createIfMissing?: boolean },
24
+ ): Promise<string | null> {
25
+ const scopedTenantId = tenantId ?? null
26
+ if (!service || !service.isEnabled() || !scopedTenantId) return null
27
+ if (cache?.has(scopedTenantId)) return cache.get(scopedTenantId) ?? null
28
+ const dek = await service.getDek(scopedTenantId)
29
+ let key = dek?.key ?? null
30
+ if (!key && opts?.createIfMissing && typeof service.createDek === 'function') {
31
+ const created = await service.createDek(scopedTenantId)
32
+ key = created?.key ?? null
33
+ }
34
+ cache?.set(scopedTenantId, key)
35
+ return key
36
+ }
37
+
38
+ export async function encryptCustomFieldValue(
39
+ value: unknown,
40
+ tenantId: string | null | undefined,
41
+ service: TenantDataEncryptionService | null,
42
+ cache?: Map<string | null, string | null>,
43
+ ): Promise<unknown> {
44
+ if (value === undefined || value === null) return value
45
+ const key = await resolveDekKey(service, tenantId, cache, { createIfMissing: true })
46
+ if (!key) return value
47
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value)
48
+ return encryptWithAesGcm(serialized, key).value
49
+ }
50
+
51
+ export async function decryptCustomFieldValue(
52
+ value: unknown,
53
+ tenantId: string | null | undefined,
54
+ service: TenantDataEncryptionService | null,
55
+ cache?: Map<string | null, string | null>,
56
+ ): Promise<unknown> {
57
+ if (value === undefined || value === null || typeof value !== 'string') return value
58
+ const key = await resolveDekKey(service, tenantId, cache)
59
+ if (!key) return value
60
+ const decrypted = decryptWithAesGcm(value, key)
61
+ if (decrypted === null) return value
62
+ try {
63
+ return JSON.parse(decrypted)
64
+ } catch {
65
+ return decrypted
66
+ }
67
+ }
@@ -0,0 +1,39 @@
1
+ // Registration pattern for entity fields (for Turbopack compatibility)
2
+ export type EntityFieldsRegistry = Record<string, Record<string, string>>
3
+
4
+ let _entityFieldsRegistry: EntityFieldsRegistry | null = null
5
+
6
+ export function registerEntityFields(registry: EntityFieldsRegistry) {
7
+ if (_entityFieldsRegistry !== null && process.env.NODE_ENV === 'development') {
8
+ console.debug('[Bootstrap] Entity fields re-registered (this may occur during HMR)')
9
+ }
10
+ _entityFieldsRegistry = registry
11
+ }
12
+
13
+ /**
14
+ * Get registered entity fields.
15
+ *
16
+ * @param throwIfNotRegistered - If true, throws error when entity fields are not registered.
17
+ * If false, returns empty object (useful during module load).
18
+ * Default: true
19
+ */
20
+ export function getEntityFieldsRegistry(throwIfNotRegistered = true): EntityFieldsRegistry {
21
+ if (!_entityFieldsRegistry) {
22
+ if (throwIfNotRegistered) {
23
+ throw new Error('[Bootstrap] Entity fields not registered. Call registerEntityFields() at bootstrap.')
24
+ }
25
+ return {} as EntityFieldsRegistry
26
+ }
27
+ return _entityFieldsRegistry
28
+ }
29
+
30
+ /**
31
+ * Get fields for a specific entity by slug.
32
+ *
33
+ * @param slug - The entity slug (e.g., 'user', 'sales_order')
34
+ * @returns The entity's fields or undefined if not found
35
+ */
36
+ export function getEntityFields(slug: string): Record<string, string> | undefined {
37
+ const registry = getEntityFieldsRegistry(false)
38
+ return registry[slug]
39
+ }
@@ -0,0 +1,107 @@
1
+ import type { EntityMetadata } from '@mikro-orm/core'
2
+
3
+ // Registration pattern for publishable packages
4
+ export type EntityIds = Record<string, Record<string, string>>
5
+
6
+ let _entityIds: EntityIds | null = null
7
+ let _entityIdLookup: Map<string, string> | null = null
8
+
9
+ export function registerEntityIds(E: EntityIds) {
10
+ if (_entityIds !== null && process.env.NODE_ENV === 'development') {
11
+ console.debug('[Bootstrap] Entity IDs re-registered (this may occur during HMR)')
12
+ }
13
+ _entityIds = E
14
+ _entityIdLookup = null // Reset cache on re-registration
15
+ }
16
+
17
+ /**
18
+ * Get registered entity IDs.
19
+ *
20
+ * @param throwIfNotRegistered - If true, throws error when entity IDs are not registered.
21
+ * If false, returns empty object (useful during module load).
22
+ * Default: true
23
+ */
24
+ export function getEntityIds(throwIfNotRegistered = true): EntityIds {
25
+ if (!_entityIds) {
26
+ if (throwIfNotRegistered) {
27
+ throw new Error('[Bootstrap] Entity IDs not registered. Call registerEntityIds() at bootstrap.')
28
+ }
29
+ return {} as EntityIds
30
+ }
31
+ return _entityIds
32
+ }
33
+
34
+ const toSnake = (value: string): string =>
35
+ value
36
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
37
+ .replace(/[\s-]+/g, '_')
38
+ .replace(/__+/g, '_')
39
+ .toLowerCase()
40
+
41
+ function getEntityIdLookup(): Map<string, string> {
42
+ if (_entityIdLookup) return _entityIdLookup
43
+ const E = getEntityIds()
44
+ const map = new Map<string, string>()
45
+ for (const mod of Object.values(E || {})) {
46
+ for (const [key, entityId] of Object.entries(mod || {})) {
47
+ const snake = toSnake(key)
48
+ map.set(snake, entityId)
49
+ // Also allow the original key and PascalCase class names to resolve
50
+ map.set(key.toLowerCase(), entityId)
51
+ map.set(
52
+ key
53
+ .split('_')
54
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
55
+ .join(''),
56
+ entityId,
57
+ )
58
+ }
59
+ }
60
+ _entityIdLookup = map
61
+ return map
62
+ }
63
+
64
+ const normalizeKey = (value: string): string =>
65
+ value
66
+ .replace(/["'`]/g, '')
67
+ .replace(/[\W]+/g, '_')
68
+ .replace(/__+/g, '_')
69
+ .toLowerCase()
70
+
71
+ const maybeSingularize = (value: string): string => {
72
+ if (value.endsWith('ies')) return `${value.slice(0, -3)}y`
73
+ if (value.endsWith('s')) return value.slice(0, -1)
74
+ return value
75
+ }
76
+
77
+ export function resolveEntityIdFromMetadata(meta: EntityMetadata<any> | undefined): string | null {
78
+ if (!meta) return null
79
+ const candidates = [
80
+ (meta as any).className,
81
+ meta.name,
82
+ (meta as any).collection,
83
+ (meta as any).tableName,
84
+ ].filter(Boolean) as string[]
85
+
86
+ for (const raw of candidates) {
87
+ const normalized = normalizeKey(raw)
88
+ const singular = maybeSingularize(normalized)
89
+ const snake = toSnake(raw)
90
+ const snakeSingular = maybeSingularize(snake)
91
+ const variants = [
92
+ normalized,
93
+ singular,
94
+ normalized.replace(/_/g, ''), // Pascal-ish fallback
95
+ singular.replace(/_/g, ''),
96
+ snake,
97
+ snakeSingular,
98
+ ]
99
+ const lookup = getEntityIdLookup()
100
+ for (const candidate of variants) {
101
+ if (!candidate) continue
102
+ const id = lookup.get(candidate)
103
+ if (id) return id
104
+ }
105
+ }
106
+ return null
107
+ }