@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,607 @@
1
+ import type { CustomFieldSet, EntityId } from '@open-mercato/shared/modules/entities'
2
+ import type { EntityManager } from '@mikro-orm/core'
3
+ import { CustomFieldDef, CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'
4
+ import type { WhereValue } from '@open-mercato/shared/lib/query/types'
5
+ import type { TenantDataEncryptionService } from '../encryption/tenantDataEncryptionService'
6
+ import { decryptCustomFieldValue, resolveTenantEncryptionService } from '../encryption/customFieldValues'
7
+ import { parseBooleanToken } from '../boolean'
8
+ import { extractCustomFieldEntries } from './custom-fields-client'
9
+
10
+ export type CustomFieldSelectors = {
11
+ keys: string[]
12
+ selectors: string[] // e.g. ['cf:priority', 'cf:severity']
13
+ outputKeys: string[] // e.g. ['cf_priority', 'cf_severity']
14
+ }
15
+
16
+ export type SplitCustomFieldPayload = {
17
+ base: Record<string, unknown>
18
+ custom: Record<string, unknown>
19
+ }
20
+
21
+ export type CustomFieldDefinitionSummary = {
22
+ key: string
23
+ label: string | null
24
+ kind: string | null
25
+ multi: boolean
26
+ dictionaryId?: string | null
27
+ organizationId?: string | null
28
+ tenantId?: string | null
29
+ priority: number
30
+ updatedAt: number
31
+ }
32
+
33
+ export type CustomFieldDefinitionIndex = Map<string, CustomFieldDefinitionSummary[]>
34
+
35
+ export type CustomFieldDisplayEntry = {
36
+ key: string
37
+ label: string | null
38
+ value: unknown
39
+ kind: string | null
40
+ multi: boolean
41
+ }
42
+
43
+ export type CustomFieldDisplayPayload = {
44
+ customValues: Record<string, unknown> | null
45
+ customFields: CustomFieldDisplayEntry[]
46
+ }
47
+
48
+ export type CustomFieldSnapshot = {
49
+ entries: Record<string, unknown>
50
+ customValues: Record<string, unknown> | null
51
+ customFields: CustomFieldDisplayEntry[]
52
+ }
53
+
54
+ export function buildCustomFieldSelectorsForEntity(entityId: EntityId, sets: CustomFieldSet[]): CustomFieldSelectors {
55
+ const keys = Array.from(new Set(
56
+ (sets || [])
57
+ .filter((s) => s.entity === entityId)
58
+ .flatMap((s) => (s.fields || []).map((f) => f.key))
59
+ ))
60
+ const selectors = keys.map((k) => `cf:${k}`)
61
+ const outputKeys = keys.map((k) => `cf_${k}`)
62
+ return { keys, selectors, outputKeys }
63
+ }
64
+
65
+ export function normalizeCustomFieldValue(val: unknown): unknown {
66
+ if (Array.isArray(val)) return val
67
+ if (typeof val === 'string') {
68
+ const s = val.trim()
69
+ // Parse Postgres array-like '{a,b,c}' to string[] when present
70
+ if (s.startsWith('{') && s.endsWith('}')) {
71
+ const inner = s.slice(1, -1).trim()
72
+ if (!inner) return []
73
+ return inner.split(/[\s,]+/).map((x) => x.trim()).filter(Boolean)
74
+ }
75
+ return s
76
+ }
77
+ return val as any
78
+ }
79
+
80
+ // Extracts cf_* fields from a record that may contain both 'cf:<key>' and/or 'cf_<key>'
81
+ export function extractCustomFieldsFromItem(item: Record<string, unknown>, keys: string[]): Record<string, unknown> {
82
+ const out: Record<string, unknown> = {}
83
+ for (const key of keys) {
84
+ const colon = item[`cf:${key}` as keyof typeof item]
85
+ const snake = item[`cf_${key}` as keyof typeof item]
86
+ const value = colon !== undefined ? colon : snake
87
+ if (value !== undefined) out[`cf_${key}`] = normalizeCustomFieldValue(value)
88
+ }
89
+ return out
90
+ }
91
+
92
+ export function extractAllCustomFieldEntries(item: Record<string, unknown>): Record<string, unknown> {
93
+ return extractCustomFieldEntries(item)
94
+ }
95
+
96
+ function normalizeFieldsetFilter(input?: string | string[] | null): Set<string | null> | null {
97
+ if (input == null) return null
98
+ const values = Array.isArray(input) ? input : [input]
99
+ const normalized = new Set<string | null>()
100
+ for (const raw of values) {
101
+ if (raw == null) continue
102
+ const trimmed = String(raw).trim()
103
+ if (!trimmed) {
104
+ normalized.add(null)
105
+ } else {
106
+ normalized.add(trimmed)
107
+ }
108
+ }
109
+ return normalized.size ? normalized : null
110
+ }
111
+
112
+ export async function buildCustomFieldFiltersFromQuery(opts: {
113
+ entityId?: EntityId
114
+ entityIds?: EntityId[]
115
+ query: Record<string, unknown>
116
+ em: EntityManager
117
+ tenantId: string | null | undefined
118
+ fieldset?: string | string[] | null
119
+ }): Promise<Record<string, WhereValue>> {
120
+ const out: Record<string, WhereValue> = {}
121
+ const entries = Object.entries(opts.query).filter(([k]) => k.startsWith('cf_'))
122
+ if (!entries.length) return out
123
+
124
+ const entityIdList = Array.isArray(opts.entityIds) && opts.entityIds.length
125
+ ? opts.entityIds
126
+ : opts.entityId
127
+ ? [opts.entityId]
128
+ : []
129
+ if (!entityIdList.length) return out
130
+
131
+ // Tenant-only scope: allow global (null) or tenant match; ignore organization here
132
+ const defs = await opts.em.find(CustomFieldDef, {
133
+ entityId: { $in: entityIdList as any },
134
+ isActive: true,
135
+ $and: [
136
+ { $or: [ { tenantId: opts.tenantId as any }, { tenantId: null } ] },
137
+ ],
138
+ })
139
+ const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)
140
+ const order = new Map<string, number>()
141
+ entityIdList.map(String).forEach((id, index) => order.set(id, index))
142
+ const byKey: Record<string, { kind: string; multi?: boolean; entityId: string }> = {}
143
+ for (const d of defs) {
144
+ if (fieldsetFilter) {
145
+ const rawFieldset = typeof d.configJson?.fieldset === 'string' ? d.configJson.fieldset.trim() : ''
146
+ const normalizedFieldset = rawFieldset.length ? rawFieldset : null
147
+ if (!fieldsetFilter.has(normalizedFieldset)) continue
148
+ }
149
+ const key = d.key
150
+ const entityId = String(d.entityId)
151
+ const current = byKey[key]
152
+ const rankNew = order.get(entityId) ?? Number.MAX_SAFE_INTEGER
153
+ if (!current) {
154
+ byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }
155
+ continue
156
+ }
157
+ const rankOld = order.get(current.entityId) ?? Number.MAX_SAFE_INTEGER
158
+ if (rankNew < rankOld) {
159
+ byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }
160
+ }
161
+ }
162
+
163
+ const coerce = (kind: string, v: unknown) => {
164
+ if (v == null) return v as undefined
165
+ switch (kind) {
166
+ case 'integer': return Number.parseInt(String(v), 10)
167
+ case 'float': return Number.parseFloat(String(v))
168
+ case 'boolean': return parseBooleanToken(String(v)) === true
169
+ default: return String(v)
170
+ }
171
+ }
172
+
173
+ for (const [rawKey, rawVal] of entries) {
174
+ const isIn = rawKey.endsWith('In')
175
+ const key = isIn ? rawKey.replace(/^cf_/, '').replace(/In$/, '') : rawKey.replace(/^cf_/, '')
176
+ const def = byKey[key]
177
+ const fieldId = `cf:${key}`
178
+ if (!def) continue
179
+ if (isIn) {
180
+ const list = Array.isArray(rawVal)
181
+ ? (rawVal as unknown[])
182
+ : String(rawVal)
183
+ .split(',')
184
+ .map((s) => s.trim())
185
+ .filter(Boolean)
186
+ if (list.length) out[fieldId] = { $in: list.map((x) => coerce(def.kind, x)) as (string[] | number[] | boolean[]) }
187
+ } else {
188
+ out[fieldId] = coerce(def.kind, rawVal)
189
+ }
190
+ }
191
+
192
+ return out
193
+ }
194
+
195
+ export function splitCustomFieldPayload(raw: unknown): SplitCustomFieldPayload {
196
+ const base: Record<string, unknown> = {}
197
+ const custom: Record<string, unknown> = {}
198
+ if (!raw || typeof raw !== 'object') return { base, custom }
199
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
200
+ if (key === 'customFields') {
201
+ if (Array.isArray(value)) {
202
+ value.forEach((entry) => {
203
+ if (!entry || typeof entry !== 'object') return
204
+ const entryKey = typeof (entry as any).key === 'string' ? (entry as any).key.trim() : ''
205
+ if (!entryKey) return
206
+ custom[entryKey] = (entry as any).value
207
+ })
208
+ continue
209
+ }
210
+ if (value && typeof value === 'object') {
211
+ for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {
212
+ const normalizedKey = typeof ck === 'string' ? ck.trim() : ''
213
+ if (!normalizedKey) continue
214
+ custom[normalizedKey] = cv
215
+ }
216
+ continue
217
+ }
218
+ }
219
+ if (key === 'customValues' && value && typeof value === 'object' && !Array.isArray(value)) {
220
+ for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {
221
+ custom[String(ck)] = cv
222
+ }
223
+ continue
224
+ }
225
+ if (key.startsWith('cf_')) {
226
+ custom[key.slice(3)] = value
227
+ continue
228
+ }
229
+ if (key.startsWith('cf:')) {
230
+ custom[key.slice(3)] = value
231
+ continue
232
+ }
233
+ base[key] = value
234
+ }
235
+ return { base, custom }
236
+ }
237
+
238
+ export function extractCustomFieldValuesFromPayload(raw: Record<string, unknown>): Record<string, unknown> {
239
+ return splitCustomFieldPayload(raw).custom
240
+ }
241
+
242
+ function normalizeDefinitionKey(key: unknown): string {
243
+ if (typeof key !== 'string') return ''
244
+ const trimmed = key.trim()
245
+ return trimmed.length ? trimmed.toLowerCase() : ''
246
+ }
247
+
248
+ function normalizeDefinitionConfig(raw: unknown): Record<string, any> {
249
+ if (!raw) return {}
250
+ if (typeof raw === 'string') {
251
+ try {
252
+ const parsed = JSON.parse(raw)
253
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
254
+ return { ...(parsed as Record<string, any>) }
255
+ }
256
+ return {}
257
+ } catch {
258
+ return {}
259
+ }
260
+ }
261
+ if (typeof raw === 'object' && !Array.isArray(raw)) {
262
+ return { ...(raw as Record<string, any>) }
263
+ }
264
+ return {}
265
+ }
266
+
267
+ function summarizeDefinition(def: CustomFieldDef): CustomFieldDefinitionSummary | null {
268
+ const normalizedKey = normalizeDefinitionKey(def.key)
269
+ if (!normalizedKey) return null
270
+ const cfg = normalizeDefinitionConfig((def as any).configJson)
271
+ const label =
272
+ typeof cfg.label === 'string' && cfg.label.trim().length
273
+ ? cfg.label.trim()
274
+ : def.key
275
+ const dictionaryId =
276
+ typeof cfg.dictionaryId === 'string' && cfg.dictionaryId.trim().length
277
+ ? cfg.dictionaryId.trim()
278
+ : null
279
+ const multi =
280
+ cfg.multi !== undefined ? Boolean(cfg.multi) : false
281
+ const priority =
282
+ typeof cfg.priority === 'number' ? cfg.priority : 0
283
+ const updatedAt =
284
+ def.updatedAt instanceof Date
285
+ ? def.updatedAt.getTime()
286
+ : new Date(def.updatedAt as any).getTime()
287
+ return {
288
+ key: def.key,
289
+ label,
290
+ kind: typeof def.kind === 'string' ? def.kind : null,
291
+ multi,
292
+ dictionaryId,
293
+ organizationId: def.organizationId ?? null,
294
+ tenantId: def.tenantId ?? null,
295
+ priority,
296
+ updatedAt: Number.isNaN(updatedAt) ? 0 : updatedAt,
297
+ }
298
+ }
299
+
300
+ function sortDefinitionSummaries(defs: CustomFieldDefinitionSummary[]): CustomFieldDefinitionSummary[] {
301
+ return [...defs].sort((a, b) => {
302
+ const priorityDiff = (a.priority ?? 0) - (b.priority ?? 0)
303
+ if (priorityDiff !== 0) return priorityDiff
304
+ const updatedDiff = (b.updatedAt ?? 0) - (a.updatedAt ?? 0)
305
+ if (updatedDiff !== 0) return updatedDiff
306
+ return a.key.localeCompare(b.key)
307
+ })
308
+ }
309
+
310
+ function selectDefinitionForRecord(
311
+ defs: CustomFieldDefinitionSummary[],
312
+ organizationId: string | null,
313
+ tenantId: string | null,
314
+ ): CustomFieldDefinitionSummary | null {
315
+ if (!defs.length) return null
316
+ const prioritizedForOrg = defs.filter(
317
+ (def) => def.organizationId && organizationId && def.organizationId === organizationId,
318
+ )
319
+ if (prioritizedForOrg.length) return sortDefinitionSummaries(prioritizedForOrg)[0]
320
+ const prioritizedForTenant = defs.filter(
321
+ (def) => def.tenantId && tenantId && def.tenantId === tenantId && !def.organizationId,
322
+ )
323
+ if (prioritizedForTenant.length) return sortDefinitionSummaries(prioritizedForTenant)[0]
324
+ const global = defs.filter((def) => !def.organizationId)
325
+ if (global.length) return sortDefinitionSummaries(global)[0]
326
+ return sortDefinitionSummaries(defs)[0] ?? null
327
+ }
328
+
329
+ export async function loadCustomFieldDefinitionIndex(opts: {
330
+ em: EntityManager
331
+ entityIds: string | string[]
332
+ tenantId?: string | null | undefined
333
+ organizationIds?: Array<string | null | undefined> | null
334
+ }): Promise<CustomFieldDefinitionIndex> {
335
+ const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds]
336
+ const entityIds = list
337
+ .map((id) => (typeof id === 'string' ? id.trim() : String(id ?? '')))
338
+ .filter((id) => id.length > 0)
339
+ if (!entityIds.length) return new Map()
340
+ const tenantId = opts.tenantId ?? null
341
+ const orgCandidates = Array.isArray(opts.organizationIds)
342
+ ? opts.organizationIds
343
+ .map((id) => (typeof id === 'string' ? id.trim() : id))
344
+ .filter((id): id is string => typeof id === 'string' && id.length > 0)
345
+ : []
346
+ const scopeClauses: Record<string, unknown>[] = [
347
+ tenantId
348
+ ? { $or: [{ tenantId: tenantId as any }, { tenantId: null }] }
349
+ : { tenantId: null },
350
+ ]
351
+ if (orgCandidates.length) {
352
+ scopeClauses.push({
353
+ $or: [{ organizationId: { $in: orgCandidates as any } }, { organizationId: null }],
354
+ })
355
+ } else {
356
+ scopeClauses.push({ organizationId: null })
357
+ }
358
+ const where: Record<string, unknown> = {
359
+ entityId: { $in: entityIds as any },
360
+ deletedAt: null,
361
+ isActive: true,
362
+ $and: scopeClauses,
363
+ }
364
+ const defs = await opts.em.find(CustomFieldDef, where as any)
365
+ const index: CustomFieldDefinitionIndex = new Map()
366
+ defs.forEach((def) => {
367
+ const summary = summarizeDefinition(def)
368
+ if (!summary) return
369
+ const normalizedKey = normalizeDefinitionKey(summary.key)
370
+ if (!normalizedKey) return
371
+ if (!index.has(normalizedKey)) index.set(normalizedKey, [])
372
+ index.get(normalizedKey)!.push(summary)
373
+ })
374
+ index.forEach((entries, key) => {
375
+ index.set(key, sortDefinitionSummaries(entries))
376
+ })
377
+ return index
378
+ }
379
+
380
+ export function decorateRecordWithCustomFields(
381
+ record: Record<string, unknown>,
382
+ definitions: CustomFieldDefinitionIndex,
383
+ context: {
384
+ organizationId?: string | null
385
+ tenantId?: string | null
386
+ } = {},
387
+ ): CustomFieldDisplayPayload {
388
+ const rawEntries = extractAllCustomFieldEntries(record)
389
+ if (!Object.keys(rawEntries).length) {
390
+ return { customValues: null, customFields: [] }
391
+ }
392
+ const values: Record<string, unknown> = {}
393
+ const entries: Array<{ entry: CustomFieldDisplayEntry; priority: number; updatedAt: number }> = []
394
+ const organizationId = context.organizationId ?? null
395
+ const tenantId = context.tenantId ?? null
396
+
397
+ Object.entries(rawEntries).forEach(([prefixedKey, value]) => {
398
+ const bareKey = prefixedKey.replace(/^cf_/, '')
399
+ const normalizedKey = normalizeDefinitionKey(bareKey)
400
+ if (!normalizedKey) return
401
+ values[bareKey] = value
402
+ const defsForKey = definitions.get(normalizedKey) ?? []
403
+ const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId)
404
+ const entry: CustomFieldDisplayEntry = {
405
+ key: bareKey,
406
+ label: resolvedDef?.label ?? bareKey,
407
+ value,
408
+ kind: resolvedDef?.kind ?? null,
409
+ multi: resolvedDef?.multi ?? Array.isArray(value),
410
+ }
411
+ entries.push({
412
+ entry,
413
+ priority: resolvedDef?.priority ?? Number.MAX_SAFE_INTEGER,
414
+ updatedAt: resolvedDef?.updatedAt ?? 0,
415
+ })
416
+ })
417
+
418
+ const ordered = entries
419
+ .sort((a, b) => {
420
+ const priorityDiff = a.priority - b.priority
421
+ if (priorityDiff !== 0) return priorityDiff
422
+ const updatedDiff = b.updatedAt - a.updatedAt
423
+ if (updatedDiff !== 0) return updatedDiff
424
+ return a.entry.key.localeCompare(b.entry.key)
425
+ })
426
+ .map((item) => item.entry)
427
+
428
+ return {
429
+ customValues: Object.keys(values).length ? values : null,
430
+ customFields: ordered,
431
+ }
432
+ }
433
+
434
+ export async function loadCustomFieldValues(opts: {
435
+ em: EntityManager
436
+ entityId: EntityId
437
+ recordIds: string[]
438
+ tenantIdByRecord?: Record<string, string | null | undefined>
439
+ organizationIdByRecord?: Record<string, string | null | undefined>
440
+ tenantFallbacks?: (string | null | undefined)[]
441
+ encryptionService?: TenantDataEncryptionService | null
442
+ }): Promise<Record<string, Record<string, unknown>>> {
443
+ const { em, entityId, recordIds } = opts
444
+ if (!Array.isArray(recordIds) || recordIds.length === 0) return {}
445
+
446
+ const normalizedRecordIds = recordIds.map((id) => String(id))
447
+ let encryptionService: TenantDataEncryptionService | null | undefined
448
+ const encryptionCache = new Map<string | null, string | null>()
449
+ const getEncryptionService = () => {
450
+ if (encryptionService !== undefined) return encryptionService
451
+ encryptionService = resolveTenantEncryptionService(em, opts.encryptionService)
452
+ return encryptionService
453
+ }
454
+ const tenantCandidates = new Set<string | null>()
455
+ tenantCandidates.add(null)
456
+ if (opts.tenantIdByRecord) {
457
+ for (const val of Object.values(opts.tenantIdByRecord)) {
458
+ tenantCandidates.add(val ? String(val) : null)
459
+ }
460
+ }
461
+ if (opts.tenantFallbacks) {
462
+ for (const val of opts.tenantFallbacks) tenantCandidates.add(val ? String(val) : null)
463
+ }
464
+ const fallbackTenant = (opts.tenantFallbacks || []).find((t) => t != null) ?? null
465
+
466
+ const tenantList = Array.from(tenantCandidates)
467
+ const tenantNonNull = tenantList.filter((t): t is string => t !== null)
468
+ const tenantFilter = tenantNonNull.length
469
+ ? { tenantId: { $in: [...tenantNonNull, null] as any } }
470
+ : { tenantId: null }
471
+ const cfRows = await em.find(CustomFieldValue, {
472
+ entityId: entityId as any,
473
+ recordId: { $in: normalizedRecordIds as any },
474
+ deletedAt: null,
475
+ ...(tenantList.length ? tenantFilter : {}),
476
+ })
477
+
478
+ if (!cfRows.length) return {}
479
+
480
+ const allKeys = Array.from(new Set(cfRows.map((row) => String(row.fieldKey))))
481
+ const organizationCandidates = new Set<string | null>()
482
+ organizationCandidates.add(null)
483
+ if (opts.organizationIdByRecord) {
484
+ for (const val of Object.values(opts.organizationIdByRecord)) {
485
+ organizationCandidates.add(val ? String(val) : null)
486
+ }
487
+ }
488
+ for (const row of cfRows) {
489
+ organizationCandidates.add(row.organizationId ? String(row.organizationId) : null)
490
+ }
491
+ const orgList = Array.from(organizationCandidates)
492
+
493
+ const defs = allKeys.length
494
+ ? await em.find(CustomFieldDef, {
495
+ entityId: entityId as any,
496
+ key: { $in: allKeys as any },
497
+ deletedAt: null,
498
+ isActive: true,
499
+ ...(tenantList.length ? { tenantId: tenantFilter.tenantId } : {}),
500
+ organizationId: { $in: orgList as any },
501
+ })
502
+ : []
503
+
504
+ const defsByKey = new Map<string, CustomFieldDef[]>()
505
+ for (const def of defs) {
506
+ const list = defsByKey.get(def.key) || []
507
+ list.push(def)
508
+ defsByKey.set(def.key, list)
509
+ }
510
+
511
+ const pickDefinition = (fieldKey: string, organizationId: string | null, tenantId: string | null) => {
512
+ const candidates = defsByKey.get(fieldKey)
513
+ if (!candidates || candidates.length === 0) return null
514
+ const active = candidates.filter((opt) => opt.isActive !== false && !opt.deletedAt)
515
+ const list = active.length ? active : candidates
516
+ if (organizationId && tenantId) {
517
+ const exact = list.find((opt) => opt.organizationId === organizationId && opt.tenantId === tenantId)
518
+ if (exact) return exact
519
+ }
520
+ if (organizationId) {
521
+ const orgMatch = list.find((opt) => opt.organizationId === organizationId && (!tenantId || opt.tenantId == null || opt.tenantId === tenantId))
522
+ if (orgMatch) return orgMatch
523
+ }
524
+ if (tenantId) {
525
+ const tenantMatch = list.find((opt) => opt.organizationId == null && opt.tenantId === tenantId)
526
+ if (tenantMatch) return tenantMatch
527
+ }
528
+ const global = list.find((opt) => opt.organizationId == null && opt.tenantId == null)
529
+ return global ?? list[0]
530
+ }
531
+
532
+ const valueFromRow = (row: CustomFieldValue): unknown => {
533
+ if (row.valueMultiline !== null && row.valueMultiline !== undefined) return row.valueMultiline
534
+ if (row.valueText !== null && row.valueText !== undefined) return row.valueText
535
+ if (row.valueInt !== null && row.valueInt !== undefined) return row.valueInt
536
+ if (row.valueFloat !== null && row.valueFloat !== undefined) return row.valueFloat
537
+ if (row.valueBool !== null && row.valueBool !== undefined) return row.valueBool
538
+ return null
539
+ }
540
+
541
+ type Bucket = { orgId: string | null; tenantId: string | null; values: unknown[]; def?: CustomFieldDef | null; encrypted?: boolean }
542
+ const buckets = new Map<string, Bucket>()
543
+
544
+ for (const row of cfRows) {
545
+ const recordId = String(row.recordId)
546
+ const key = String(row.fieldKey)
547
+ const bucketKey = `${recordId}::${key}`
548
+ const orgId = row.organizationId ? String(row.organizationId) : null
549
+ const tenantId = row.tenantId ? String(row.tenantId) : null
550
+ const resolvedOrgId = orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null)
551
+ const resolvedTenantId = tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? fallbackTenant)
552
+ const def = pickDefinition(key, resolvedOrgId, resolvedTenantId)
553
+ const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)
554
+ const value = valueFromRow(row)
555
+ const decrypted = encrypted
556
+ ? await decryptCustomFieldValue(value, resolvedTenantId ?? tenantId ?? null, getEncryptionService(), encryptionCache)
557
+ : value
558
+ const existing = buckets.get(bucketKey)
559
+ if (existing) {
560
+ if (existing.orgId == null && resolvedOrgId) existing.orgId = resolvedOrgId
561
+ if (existing.tenantId == null && resolvedTenantId) existing.tenantId = resolvedTenantId
562
+ if (existing.def == null && def) existing.def = def
563
+ existing.encrypted = existing.encrypted || encrypted
564
+ existing.values.push(decrypted)
565
+ } else {
566
+ buckets.set(bucketKey, { orgId: resolvedOrgId, tenantId: resolvedTenantId, values: [decrypted], def: def ?? null, encrypted })
567
+ }
568
+ }
569
+
570
+ const result: Record<string, Record<string, unknown>> = {}
571
+ for (const [compoundKey, bucket] of buckets.entries()) {
572
+ const [recordId, fieldKey] = compoundKey.split('::')
573
+ if (!result[recordId]) result[recordId] = {}
574
+ const prefixed = `cf_${fieldKey}`
575
+ const def = bucket.def ?? pickDefinition(fieldKey, bucket.orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null), bucket.tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? null))
576
+ if (def && def.configJson && typeof def.configJson === 'object' && (def.configJson as any).multi) {
577
+ const cleaned = bucket.values.filter((v) => v !== undefined && v !== null)
578
+ result[recordId][prefixed] = cleaned
579
+ } else if (bucket.values.length > 1) {
580
+ const cleaned = bucket.values.filter((v) => v !== undefined)
581
+ result[recordId][prefixed] = cleaned
582
+ } else {
583
+ result[recordId][prefixed] = bucket.values[0] ?? null
584
+ }
585
+ }
586
+
587
+ return result
588
+ }
589
+
590
+ export function summarizeCustomFields(record: Record<string, unknown>): CustomFieldSnapshot {
591
+ const entries = extractAllCustomFieldEntries(record)
592
+ const values = Object.fromEntries(
593
+ Object.entries(entries).map(([prefixedKey, value]) => [
594
+ prefixedKey.replace(/^cf_/, ''),
595
+ value,
596
+ ]),
597
+ )
598
+ const customValues = Object.keys(values).length ? values : null
599
+ const customFields = Object.entries(values).map(([key, value]) => ({
600
+ key,
601
+ label: key,
602
+ value,
603
+ kind: null,
604
+ multi: Array.isArray(value),
605
+ }))
606
+ return { entries, customValues, customFields }
607
+ }
@@ -0,0 +1,23 @@
1
+ export class CrudHttpError extends Error {
2
+ status: number
3
+ body: Record<string, any>
4
+
5
+ constructor(status: number, body?: Record<string, any> | string) {
6
+ const normalizedBody = typeof body === 'string' ? { error: body } : body ?? {}
7
+ super(typeof body === 'string' ? body : normalizedBody.error ?? 'Request failed')
8
+ this.status = status
9
+ this.body = normalizedBody
10
+ }
11
+ }
12
+
13
+ export function badRequest(message: string): CrudHttpError {
14
+ return new CrudHttpError(400, { error: message })
15
+ }
16
+
17
+ export function forbidden(message = 'Forbidden'): CrudHttpError {
18
+ return new CrudHttpError(403, { error: message })
19
+ }
20
+
21
+ export function notFound(message = 'Not found'): CrudHttpError {
22
+ return new CrudHttpError(404, { error: message })
23
+ }