@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,127 @@
1
+ import type { CacheStrategy } from '@open-mercato/cache'
2
+ import { analyzeCacheSegments, purgeCacheSegment } from '../cache/segments'
3
+
4
+ export type CrudCacheSegmentInfo = {
5
+ segment: string
6
+ resource: string | null
7
+ method: string | null
8
+ path: string | null
9
+ keyCount: number
10
+ keys: string[]
11
+ }
12
+
13
+ export type CrudCacheStats = {
14
+ generatedAt: string
15
+ segments: CrudCacheSegmentInfo[]
16
+ totalKeys: number
17
+ }
18
+
19
+ const CRUD_CACHE_PATTERN = 'crud|*'
20
+ export const CRUD_CACHE_STATS_KEY = 'crud-cache-stats'
21
+
22
+ function sanitizeSegment(value: string): string {
23
+ return value
24
+ .replace(/[^a-zA-Z0-9:_/|-]/g, '-')
25
+ .replace(/-{2,}/g, '-')
26
+ .replace(/^-+|-+$/g, '')
27
+ }
28
+
29
+ function normalizePathSegment(path: string | null | undefined, resource: string | null | undefined): string | null {
30
+ if (!path) return null
31
+ let trimmed = path.trim()
32
+ if (!trimmed) return null
33
+ trimmed = trimmed.replace(/^https?:\/\/[^/]+/i, '')
34
+ trimmed = trimmed.replace(/^\/+/, '')
35
+ if (trimmed.startsWith('backend/')) trimmed = trimmed.slice('backend/'.length)
36
+ if (trimmed.startsWith('api/')) trimmed = trimmed.slice('api/'.length)
37
+ trimmed = trimmed.replace(/^\/+/, '').replace(/\/+/g, '/')
38
+ trimmed = trimmed.replace(/\/$/, '')
39
+ if (!trimmed) return null
40
+ const segments = trimmed.split('/').filter(Boolean)
41
+ if (!segments.length) return null
42
+ const resourceSegments = resource ? resource.split('.').filter(Boolean) : []
43
+ if (resourceSegments.length && segments[0] === resourceSegments[0]) {
44
+ segments.shift()
45
+ }
46
+ const formatted = segments.join('-')
47
+ return formatted.length ? sanitizeSegment(formatted) : null
48
+ }
49
+
50
+ function parseCrudCacheKey(key: string): {
51
+ resource: string | null
52
+ method: string | null
53
+ path: string | null
54
+ segment: string | null
55
+ } {
56
+ const parts = key.split('|')
57
+ if (parts.length < 4) {
58
+ return { resource: null, method: null, path: null, segment: null }
59
+ }
60
+ const resource = parts[1] ?? null
61
+ const method = parts[2] ?? null
62
+ const path = parts[3] ?? null
63
+ const normalizedPath = normalizePathSegment(path, resource)
64
+ const fallback = resource ? sanitizeSegment(resource.replace(/[.]/g, '-')) : null
65
+ const segment = normalizedPath ?? fallback
66
+ return {
67
+ resource,
68
+ method,
69
+ path,
70
+ segment,
71
+ }
72
+ }
73
+
74
+ export function deriveCrudSegmentTag(resource: string, request: Request): string {
75
+ const url = new URL(request.url)
76
+ const pathSegment = normalizePathSegment(url.pathname, resource)
77
+ const fallback = sanitizeSegment(resource.replace(/[.]/g, '-'))
78
+ const finalSegment = pathSegment ?? fallback
79
+ return finalSegment || 'crud-root'
80
+ }
81
+
82
+ export async function collectCrudCacheStats(cache: CacheStrategy): Promise<CrudCacheStats> {
83
+ const analyses = await analyzeCacheSegments(cache, {
84
+ keysPattern: CRUD_CACHE_PATTERN,
85
+ deriveSegment: (key) => parseCrudCacheKey(key).segment,
86
+ filterKey: (key) => key !== CRUD_CACHE_STATS_KEY,
87
+ })
88
+
89
+ const segments: CrudCacheSegmentInfo[] = analyses.map(({ segment, keys }) => {
90
+ const sample = keys[0] ?? null
91
+ const details = sample ? parseCrudCacheKey(sample) : { resource: null, method: null, path: null, segment: null }
92
+ return {
93
+ segment,
94
+ resource: details.resource,
95
+ method: details.method,
96
+ path: details.path,
97
+ keyCount: keys.length,
98
+ keys,
99
+ }
100
+ })
101
+
102
+ const stats: CrudCacheStats = {
103
+ generatedAt: new Date().toISOString(),
104
+ segments,
105
+ totalKeys: segments.reduce((sum, entry) => sum + entry.keyCount, 0),
106
+ }
107
+
108
+ try {
109
+ await cache.set(CRUD_CACHE_STATS_KEY, stats, { tags: ['crud-cache-stats'] })
110
+ } catch {
111
+ // best effort write; ignore failure
112
+ }
113
+
114
+ return stats
115
+ }
116
+
117
+ export async function purgeCrudCacheSegment(cache: CacheStrategy, segment: string): Promise<{ deleted: number; keys: string[] }> {
118
+ return purgeCacheSegment(
119
+ cache,
120
+ {
121
+ keysPattern: CRUD_CACHE_PATTERN,
122
+ deriveSegment: (key) => parseCrudCacheKey(key).segment,
123
+ filterKey: (key) => key !== CRUD_CACHE_STATS_KEY,
124
+ },
125
+ segment,
126
+ )
127
+ }
@@ -0,0 +1,205 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import type { CacheStrategy } from '@open-mercato/cache'
3
+ import { parseBooleanToken } from '../boolean'
4
+
5
+ export type CrudCacheIdentifiers = {
6
+ id?: string | null
7
+ organizationId?: string | null
8
+ tenantId?: string | null
9
+ }
10
+
11
+ let crudCacheEnabledFlag: boolean | null = null
12
+ export function isCrudCacheEnabled(): boolean {
13
+ if (crudCacheEnabledFlag !== null) return crudCacheEnabledFlag
14
+ crudCacheEnabledFlag = parseBooleanToken(process.env.ENABLE_CRUD_API_CACHE ?? '') === true
15
+ return crudCacheEnabledFlag
16
+ }
17
+
18
+ let crudCacheDebugFlag: boolean | null = null
19
+ export function isCrudCacheDebugEnabled(): boolean {
20
+ if (crudCacheDebugFlag !== null) return crudCacheDebugFlag
21
+ crudCacheDebugFlag = parseBooleanToken(process.env.QUERY_ENGINE_DEBUG_SQL ?? '') === true
22
+ return crudCacheDebugFlag
23
+ }
24
+
25
+ export function debugCrudCache(event: string, context: Record<string, unknown>) {
26
+ if (!isCrudCacheDebugEnabled()) return
27
+ try {
28
+ console.debug('[crud][cache]', event, context)
29
+ } catch {}
30
+ }
31
+
32
+ export function resolveCrudCache(container: AwilixContainer): CacheStrategy | null {
33
+ try {
34
+ const cache = (container.resolve('cache') as CacheStrategy)
35
+ if (cache && typeof cache.get === 'function' && typeof cache.set === 'function') {
36
+ return cache
37
+ }
38
+ } catch {}
39
+ return null
40
+ }
41
+
42
+ export function normalizeTagSegment(value: string | null | undefined): string {
43
+ if (value === null || value === undefined || value === '') return 'null'
44
+ return value.toString().trim().replace(/[^a-zA-Z0-9._-]/g, '-')
45
+ }
46
+
47
+ export function canonicalizeResourceTag(value: string | null | undefined): string | null {
48
+ if (value === null || value === undefined) return null
49
+ const trimmed = String(value).trim()
50
+ if (!trimmed.length) return null
51
+ const withSeparators = trimmed
52
+ .replace(/::/g, '.')
53
+ .replace(/[/\\]+/g, '.')
54
+ .replace(/[\s]+/g, '.')
55
+ .replace(/_/g, '.')
56
+ .replace(/-+/g, '.')
57
+ const withCamelBreaks = withSeparators.replace(/([a-z0-9])([A-Z])/g, '$1.$2')
58
+ const collapsed = withCamelBreaks.replace(/\.{2,}/g, '.').replace(/^\.+|\.+$/g, '')
59
+ const lowered = collapsed.toLowerCase()
60
+ return lowered.length ? lowered : null
61
+ }
62
+
63
+ export function buildRecordTag(resource: string, tenantId: string | null, recordId: string): string {
64
+ return [
65
+ 'crud',
66
+ normalizeTagSegment(resource),
67
+ 'tenant',
68
+ normalizeTagSegment(tenantId),
69
+ 'record',
70
+ normalizeTagSegment(recordId),
71
+ ].join(':')
72
+ }
73
+
74
+ export function buildCollectionTags(
75
+ resource: string,
76
+ tenantId: string | null,
77
+ organizationIds: Array<string | null>
78
+ ): string[] {
79
+ const normalizedResource = normalizeTagSegment(resource)
80
+ const normalizedTenant = normalizeTagSegment(tenantId)
81
+ const tags = new Set<string>()
82
+ if (!organizationIds.length) {
83
+ tags.add(['crud', normalizedResource, 'tenant', normalizedTenant, 'org', 'null', 'collection'].join(':'))
84
+ return Array.from(tags)
85
+ }
86
+ for (const orgId of organizationIds) {
87
+ tags.add(['crud', normalizedResource, 'tenant', normalizedTenant, 'org', normalizeTagSegment(orgId), 'collection'].join(':'))
88
+ }
89
+ return Array.from(tags)
90
+ }
91
+
92
+ export function normalizeIdentifierValue(value: unknown): string | null {
93
+ if (value === null || value === undefined) return null
94
+ if (typeof value === 'string') return value
95
+ if (typeof value === 'number' || typeof value === 'bigint') return String(value)
96
+ if (typeof value === 'object') {
97
+ if (value instanceof Date) return value.toISOString()
98
+ if (value && typeof (value as { id?: unknown }).id !== 'undefined') {
99
+ return normalizeIdentifierValue((value as { id?: unknown }).id)
100
+ }
101
+ }
102
+ return String(value)
103
+ }
104
+
105
+ export function pickFirstIdentifier(...values: Array<unknown>): string | null {
106
+ for (const value of values) {
107
+ const normalized = normalizeIdentifierValue(value)
108
+ if (normalized) return normalized
109
+ }
110
+ return null
111
+ }
112
+
113
+ function singularizeSegment(segment: string): string {
114
+ const lower = segment.toLowerCase()
115
+ if (lower.endsWith('ies') && lower.length > 3) return lower.slice(0, -3) + 'y'
116
+ if (lower.endsWith('ses') && lower.length > 3) return lower.slice(0, -2)
117
+ if (
118
+ (lower.endsWith('xes') ||
119
+ lower.endsWith('zes') ||
120
+ lower.endsWith('ches') ||
121
+ lower.endsWith('shes')) &&
122
+ lower.length > 3
123
+ ) {
124
+ return lower.slice(0, -2)
125
+ }
126
+ if (lower.endsWith('s') && !lower.endsWith('ss') && lower.length > 1) return lower.slice(0, -1)
127
+ return lower
128
+ }
129
+
130
+ function singularizeResourceSegment(segment: string): string {
131
+ return segment
132
+ .split('-')
133
+ .map((part) => singularizeSegment(part))
134
+ .join('-')
135
+ }
136
+
137
+ export function deriveResourceFromCommandId(commandId: string | undefined | null): string | null {
138
+ if (!commandId || typeof commandId !== 'string') return null
139
+ const parts = commandId.split('.')
140
+ if (parts.length < 2) return null
141
+ const modulePart = parts[0]
142
+ const entityPart = singularizeResourceSegment(parts[1])
143
+ if (!modulePart || !entityPart) return null
144
+ return `${modulePart}.${entityPart}`
145
+ }
146
+
147
+ export function expandResourceAliases(resource: string, aliases?: string[]): string[] {
148
+ const set = new Set<string>()
149
+ const inputs = [resource, ...(aliases ?? [])]
150
+ for (const candidate of inputs) {
151
+ const canonical = canonicalizeResourceTag(candidate)
152
+ if (canonical) set.add(canonical)
153
+ }
154
+ if (!set.size) return []
155
+ return Array.from(set)
156
+ }
157
+
158
+ export async function invalidateCrudCache(
159
+ container: AwilixContainer,
160
+ resource: string,
161
+ identifiers: CrudCacheIdentifiers,
162
+ fallbackTenant: string | null,
163
+ reason: string,
164
+ aliases?: string[]
165
+ ): Promise<void> {
166
+ if (!isCrudCacheEnabled()) return
167
+ const cache = resolveCrudCache(container)
168
+ if (!cache || typeof cache.deleteByTags !== 'function') return
169
+ const resources = expandResourceAliases(resource, aliases)
170
+ const tenantId = identifiers.tenantId ?? fallbackTenant ?? null
171
+ const recordId = normalizeIdentifierValue(identifiers.id)
172
+ const tags = new Set<string>()
173
+ for (const key of resources) {
174
+ if (recordId) {
175
+ tags.add(buildRecordTag(key, tenantId, recordId))
176
+ }
177
+ const organizationIds: Array<string | null> = []
178
+ if (identifiers.organizationId !== undefined) {
179
+ organizationIds.push(identifiers.organizationId ?? null)
180
+ }
181
+ if (!organizationIds.length) organizationIds.push(null)
182
+ for (const tag of buildCollectionTags(key, tenantId, organizationIds)) {
183
+ tags.add(tag)
184
+ }
185
+ }
186
+ if (!tags.size) return
187
+ const tagList = Array.from(tags)
188
+ debugCrudCache('invalidate', {
189
+ resource,
190
+ aliases: resources,
191
+ reason,
192
+ tenantId: tenantId ?? 'null',
193
+ tags: tagList,
194
+ action: 'clearing',
195
+ })
196
+ const deleted = await cache.deleteByTags(tagList)
197
+ debugCrudCache('invalidate', {
198
+ resource,
199
+ reason,
200
+ tenantId: tenantId ?? 'null',
201
+ tags: tagList,
202
+ action: 'cleared',
203
+ deleted,
204
+ })
205
+ }
@@ -0,0 +1,54 @@
1
+ export function extractCustomFieldEntries(item: Record<string, unknown>): Record<string, unknown> {
2
+ const out: Record<string, unknown> = {}
3
+ if (!item || typeof item !== 'object') return out
4
+
5
+ const normalizeValue = (value: unknown): unknown => {
6
+ if (Array.isArray(value)) return value
7
+ if (typeof value !== 'string') return value
8
+ const trimmed = value.trim()
9
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
10
+ const inner = trimmed.slice(1, -1).trim()
11
+ if (!inner) return []
12
+ return inner.split(/[\s,]+/).map((entry) => entry.trim()).filter(Boolean)
13
+ }
14
+ return trimmed
15
+ }
16
+
17
+ const assign = (rawKey: unknown, rawValue: unknown) => {
18
+ if (typeof rawKey !== 'string') return
19
+ const trimmed = rawKey.trim()
20
+ if (!trimmed) return
21
+ out[`cf_${trimmed}`] = normalizeValue(rawValue)
22
+ }
23
+
24
+ for (const [rawKey, rawValue] of Object.entries(item)) {
25
+ if (rawKey.startsWith('cf_')) {
26
+ if (rawKey.endsWith('__is_multi')) continue
27
+ out[rawKey] = normalizeValue(rawValue)
28
+ } else if (rawKey.startsWith('cf:')) {
29
+ assign(rawKey.slice(3), rawValue)
30
+ }
31
+ }
32
+
33
+ const customValues = (item as any).customValues
34
+ if (customValues && typeof customValues === 'object' && !Array.isArray(customValues)) {
35
+ for (const [key, value] of Object.entries(customValues as Record<string, unknown>)) {
36
+ assign(key, value)
37
+ }
38
+ }
39
+
40
+ const customFields = (item as any).customFields
41
+ if (Array.isArray(customFields)) {
42
+ for (const entry of customFields as Array<Record<string, unknown>>) {
43
+ const key = entry && typeof entry.key === 'string' ? entry.key : null
44
+ if (!key) continue
45
+ assign(key, 'value' in entry ? (entry as any).value : undefined)
46
+ }
47
+ } else if (customFields && typeof customFields === 'object') {
48
+ for (const [key, value] of Object.entries(customFields as Record<string, unknown>)) {
49
+ assign(key, value)
50
+ }
51
+ }
52
+
53
+ return out
54
+ }