@open-mercato/shared 0.4.5-develop-03023b2707 → 0.4.5-develop-0c30cb4b11

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 (36) hide show
  1. package/dist/lib/bootstrap/factory.js +4 -0
  2. package/dist/lib/bootstrap/factory.js.map +2 -2
  3. package/dist/lib/crud/enricher-registry.js +47 -0
  4. package/dist/lib/crud/enricher-registry.js.map +7 -0
  5. package/dist/lib/crud/enricher-runner.js +242 -0
  6. package/dist/lib/crud/enricher-runner.js.map +7 -0
  7. package/dist/lib/crud/factory.js +53 -1
  8. package/dist/lib/crud/factory.js.map +2 -2
  9. package/dist/lib/crud/response-enricher.js +1 -0
  10. package/dist/lib/crud/response-enricher.js.map +7 -0
  11. package/dist/lib/version.js +1 -1
  12. package/dist/lib/version.js.map +1 -1
  13. package/dist/modules/events/factory.js +5 -0
  14. package/dist/modules/events/factory.js.map +2 -2
  15. package/dist/modules/registry.js.map +1 -1
  16. package/dist/modules/widgets/injection-loader.js +100 -40
  17. package/dist/modules/widgets/injection-loader.js.map +2 -2
  18. package/dist/modules/widgets/injection-position.js +48 -0
  19. package/dist/modules/widgets/injection-position.js.map +7 -0
  20. package/dist/modules/widgets/injection-progress.js +1 -0
  21. package/dist/modules/widgets/injection-progress.js.map +7 -0
  22. package/package.json +1 -1
  23. package/src/lib/bootstrap/factory.ts +6 -0
  24. package/src/lib/bootstrap/types.ts +6 -0
  25. package/src/lib/crud/enricher-registry.ts +68 -0
  26. package/src/lib/crud/enricher-runner.ts +329 -0
  27. package/src/lib/crud/factory.ts +79 -1
  28. package/src/lib/crud/response-enricher.ts +110 -0
  29. package/src/modules/events/factory.ts +9 -0
  30. package/src/modules/events/types.ts +2 -0
  31. package/src/modules/registry.ts +2 -2
  32. package/src/modules/widgets/__tests__/injection-position.test.ts +33 -0
  33. package/src/modules/widgets/injection-loader.ts +140 -50
  34. package/src/modules/widgets/injection-position.ts +59 -0
  35. package/src/modules/widgets/injection-progress.ts +35 -0
  36. package/src/modules/widgets/injection.ts +280 -3
@@ -5,6 +5,7 @@ import { registerEntityIds } from "../encryption/entityIds.js";
5
5
  import { registerEntityFields } from "../encryption/entityFields.js";
6
6
  import { registerSearchModuleConfigs } from "../../modules/search.js";
7
7
  import { registerAnalyticsModuleConfigs } from "../../modules/analytics.js";
8
+ import { registerResponseEnrichers } from "../crud/enricher-registry.js";
8
9
  let _bootstrapped = false;
9
10
  let _asyncRegistrationPromise = null;
10
11
  function createBootstrap(data, options = {}) {
@@ -24,6 +25,9 @@ function createBootstrap(data, options = {}) {
24
25
  if (data.analyticsModuleConfigs) {
25
26
  registerAnalyticsModuleConfigs(data.analyticsModuleConfigs);
26
27
  }
28
+ if (data.enricherEntries) {
29
+ registerResponseEnrichers(data.enricherEntries);
30
+ }
27
31
  _asyncRegistrationPromise = registerWidgetsAndOptionalPackages(data, options);
28
32
  void _asyncRegistrationPromise;
29
33
  options.onRegistrationComplete?.();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/bootstrap/factory.ts"],
4
- "sourcesContent": ["import type { BootstrapData, BootstrapOptions } from './types'\nimport { registerOrmEntities } from '../db/mikro'\nimport { registerDiRegistrars } from '../di/container'\nimport { registerModules } from '../modules/registry'\nimport { registerEntityIds } from '../encryption/entityIds'\nimport { registerEntityFields } from '../encryption/entityFields'\nimport { registerSearchModuleConfigs } from '../../modules/search'\nimport { registerAnalyticsModuleConfigs } from '../../modules/analytics'\n\nlet _bootstrapped = false\n\n// Store the async registration promise so callers can await it if needed\nlet _asyncRegistrationPromise: Promise<void> | null = null\n\n/**\n * Creates a bootstrap function that registers all application dependencies.\n *\n * The returned function should be called once at application startup.\n * In development mode, it can be called multiple times (for HMR).\n *\n * @param data - All generated registry data from .mercato/generated/\n * @param options - Optional configuration\n * @returns A bootstrap function to call at app startup\n */\nexport function createBootstrap(data: BootstrapData, options: BootstrapOptions = {}) {\n return function bootstrap(): void {\n // In development, always re-run registrations to handle HMR\n // (Module state may be reset when Turbopack reloads packages)\n if (_bootstrapped && process.env.NODE_ENV !== 'development') return\n _bootstrapped = true\n\n // === 1. Foundation: ORM entities and DI registrars ===\n registerOrmEntities(data.entities)\n registerDiRegistrars(data.diRegistrars.filter((r): r is NonNullable<typeof r> => r != null))\n\n // === 2. Modules registry (required by i18n, query engine, dashboards, CLI) ===\n registerModules(data.modules)\n\n // === 3. Entity IDs (required by encryption, indexing, entity links) ===\n registerEntityIds(data.entityIds)\n\n // === 4. Entity fields registry (for encryption manager, Turbopack compatibility) ===\n if (data.entityFieldsRegistry) {\n registerEntityFields(data.entityFieldsRegistry)\n }\n\n // === 5. Search module configs (for search service registration in DI) ===\n if (data.searchModuleConfigs) {\n registerSearchModuleConfigs(data.searchModuleConfigs)\n }\n\n // === 6. Analytics module configs (for dashboard widgets and analytics API) ===\n if (data.analyticsModuleConfigs) {\n registerAnalyticsModuleConfigs(data.analyticsModuleConfigs)\n }\n\n // === 7-8. UI Widgets and Optional packages (async to avoid circular deps) ===\n // Store the promise so CLI context can await it\n _asyncRegistrationPromise = registerWidgetsAndOptionalPackages(data, options)\n void _asyncRegistrationPromise\n\n options.onRegistrationComplete?.()\n }\n}\n\n/**\n * Wait for async registrations (CLI modules, widgets, etc.) to complete.\n * Call this after bootstrap() in CLI context where you need modules immediately.\n */\nexport async function waitForAsyncRegistration(): Promise<void> {\n if (_asyncRegistrationPromise) {\n await _asyncRegistrationPromise\n }\n}\n\nasync function registerWidgetsAndOptionalPackages(data: BootstrapData, options: BootstrapOptions): Promise<void> {\n // Register UI widgets (dynamic imports to avoid circular deps with ui/core packages)\n try {\n const [dashboardRegistry, injectionRegistry, coreInjection] = await Promise.all([\n import('@open-mercato/ui/backend/dashboard/widgetRegistry'),\n import('@open-mercato/ui/backend/injection/widgetRegistry'),\n import('@open-mercato/core/modules/widgets/lib/injection'),\n ])\n\n dashboardRegistry.registerDashboardWidgets(data.dashboardWidgetEntries)\n injectionRegistry.registerInjectionWidgets(data.injectionWidgetEntries)\n coreInjection.registerCoreInjectionWidgets(data.injectionWidgetEntries)\n coreInjection.registerCoreInjectionTables(data.injectionTables)\n } catch {\n // UI packages may not be available in all contexts\n }\n\n // Note: Search module configs are registered synchronously in the main bootstrap.\n // The actual registerSearchModule() call happens in core/bootstrap.ts when the\n // DI container is created, using getSearchModuleConfigs() from the global registry.\n\n // Note: CLI module registration is handled separately in CLI context\n // via bootstrapFromAppRoot in dynamicLoader. We don't import CLI here\n // to avoid Turbopack tracing through the CLI package in Next.js context.\n}\n\n/**\n * Check if bootstrap has been called.\n */\nexport function isBootstrapped(): boolean {\n return _bootstrapped\n}\n\n/**\n * Reset bootstrap state. Useful for testing.\n */\nexport function resetBootstrapState(): void {\n _bootstrapped = false\n}\n"],
5
- "mappings": "AACA,SAAS,2BAA2B;AACpC,SAAS,4BAA4B;AACrC,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAClC,SAAS,4BAA4B;AACrC,SAAS,mCAAmC;AAC5C,SAAS,sCAAsC;AAE/C,IAAI,gBAAgB;AAGpB,IAAI,4BAAkD;AAY/C,SAAS,gBAAgB,MAAqB,UAA4B,CAAC,GAAG;AACnF,SAAO,SAAS,YAAkB;AAGhC,QAAI,iBAAiB,QAAQ,IAAI,aAAa,cAAe;AAC7D,oBAAgB;AAGhB,wBAAoB,KAAK,QAAQ;AACjC,yBAAqB,KAAK,aAAa,OAAO,CAAC,MAAkC,KAAK,IAAI,CAAC;AAG3F,oBAAgB,KAAK,OAAO;AAG5B,sBAAkB,KAAK,SAAS;AAGhC,QAAI,KAAK,sBAAsB;AAC7B,2BAAqB,KAAK,oBAAoB;AAAA,IAChD;AAGA,QAAI,KAAK,qBAAqB;AAC5B,kCAA4B,KAAK,mBAAmB;AAAA,IACtD;AAGA,QAAI,KAAK,wBAAwB;AAC/B,qCAA+B,KAAK,sBAAsB;AAAA,IAC5D;AAIA,gCAA4B,mCAAmC,MAAM,OAAO;AAC5E,SAAK;AAEL,YAAQ,yBAAyB;AAAA,EACnC;AACF;AAMA,eAAsB,2BAA0C;AAC9D,MAAI,2BAA2B;AAC7B,UAAM;AAAA,EACR;AACF;AAEA,eAAe,mCAAmC,MAAqB,SAA0C;AAE/G,MAAI;AACF,UAAM,CAAC,mBAAmB,mBAAmB,aAAa,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC9E,OAAO,mDAAmD;AAAA,MAC1D,OAAO,mDAAmD;AAAA,MAC1D,OAAO,kDAAkD;AAAA,IAC3D,CAAC;AAED,sBAAkB,yBAAyB,KAAK,sBAAsB;AACtE,sBAAkB,yBAAyB,KAAK,sBAAsB;AACtE,kBAAc,6BAA6B,KAAK,sBAAsB;AACtE,kBAAc,4BAA4B,KAAK,eAAe;AAAA,EAChE,QAAQ;AAAA,EAER;AASF;AAKO,SAAS,iBAA0B;AACxC,SAAO;AACT;AAKO,SAAS,sBAA4B;AAC1C,kBAAgB;AAClB;",
4
+ "sourcesContent": ["import type { BootstrapData, BootstrapOptions } from './types'\nimport { registerOrmEntities } from '../db/mikro'\nimport { registerDiRegistrars } from '../di/container'\nimport { registerModules } from '../modules/registry'\nimport { registerEntityIds } from '../encryption/entityIds'\nimport { registerEntityFields } from '../encryption/entityFields'\nimport { registerSearchModuleConfigs } from '../../modules/search'\nimport { registerAnalyticsModuleConfigs } from '../../modules/analytics'\nimport { registerResponseEnrichers } from '../crud/enricher-registry'\n\nlet _bootstrapped = false\n\n// Store the async registration promise so callers can await it if needed\nlet _asyncRegistrationPromise: Promise<void> | null = null\n\n/**\n * Creates a bootstrap function that registers all application dependencies.\n *\n * The returned function should be called once at application startup.\n * In development mode, it can be called multiple times (for HMR).\n *\n * @param data - All generated registry data from .mercato/generated/\n * @param options - Optional configuration\n * @returns A bootstrap function to call at app startup\n */\nexport function createBootstrap(data: BootstrapData, options: BootstrapOptions = {}) {\n return function bootstrap(): void {\n // In development, always re-run registrations to handle HMR\n // (Module state may be reset when Turbopack reloads packages)\n if (_bootstrapped && process.env.NODE_ENV !== 'development') return\n _bootstrapped = true\n\n // === 1. Foundation: ORM entities and DI registrars ===\n registerOrmEntities(data.entities)\n registerDiRegistrars(data.diRegistrars.filter((r): r is NonNullable<typeof r> => r != null))\n\n // === 2. Modules registry (required by i18n, query engine, dashboards, CLI) ===\n registerModules(data.modules)\n\n // === 3. Entity IDs (required by encryption, indexing, entity links) ===\n registerEntityIds(data.entityIds)\n\n // === 4. Entity fields registry (for encryption manager, Turbopack compatibility) ===\n if (data.entityFieldsRegistry) {\n registerEntityFields(data.entityFieldsRegistry)\n }\n\n // === 5. Search module configs (for search service registration in DI) ===\n if (data.searchModuleConfigs) {\n registerSearchModuleConfigs(data.searchModuleConfigs)\n }\n\n // === 6. Analytics module configs (for dashboard widgets and analytics API) ===\n if (data.analyticsModuleConfigs) {\n registerAnalyticsModuleConfigs(data.analyticsModuleConfigs)\n }\n\n // === 6b. Response enrichers (for CRUD response enrichment) ===\n if (data.enricherEntries) {\n registerResponseEnrichers(data.enricherEntries)\n }\n\n // === 7-8. UI Widgets and Optional packages (async to avoid circular deps) ===\n // Store the promise so CLI context can await it\n _asyncRegistrationPromise = registerWidgetsAndOptionalPackages(data, options)\n void _asyncRegistrationPromise\n\n options.onRegistrationComplete?.()\n }\n}\n\n/**\n * Wait for async registrations (CLI modules, widgets, etc.) to complete.\n * Call this after bootstrap() in CLI context where you need modules immediately.\n */\nexport async function waitForAsyncRegistration(): Promise<void> {\n if (_asyncRegistrationPromise) {\n await _asyncRegistrationPromise\n }\n}\n\nasync function registerWidgetsAndOptionalPackages(data: BootstrapData, options: BootstrapOptions): Promise<void> {\n // Register UI widgets (dynamic imports to avoid circular deps with ui/core packages)\n try {\n const [dashboardRegistry, injectionRegistry, coreInjection] = await Promise.all([\n import('@open-mercato/ui/backend/dashboard/widgetRegistry'),\n import('@open-mercato/ui/backend/injection/widgetRegistry'),\n import('@open-mercato/core/modules/widgets/lib/injection'),\n ])\n\n dashboardRegistry.registerDashboardWidgets(data.dashboardWidgetEntries)\n injectionRegistry.registerInjectionWidgets(data.injectionWidgetEntries)\n coreInjection.registerCoreInjectionWidgets(data.injectionWidgetEntries)\n coreInjection.registerCoreInjectionTables(data.injectionTables)\n } catch {\n // UI packages may not be available in all contexts\n }\n\n // Note: Search module configs are registered synchronously in the main bootstrap.\n // The actual registerSearchModule() call happens in core/bootstrap.ts when the\n // DI container is created, using getSearchModuleConfigs() from the global registry.\n\n // Note: CLI module registration is handled separately in CLI context\n // via bootstrapFromAppRoot in dynamicLoader. We don't import CLI here\n // to avoid Turbopack tracing through the CLI package in Next.js context.\n}\n\n/**\n * Check if bootstrap has been called.\n */\nexport function isBootstrapped(): boolean {\n return _bootstrapped\n}\n\n/**\n * Reset bootstrap state. Useful for testing.\n */\nexport function resetBootstrapState(): void {\n _bootstrapped = false\n}\n"],
5
+ "mappings": "AACA,SAAS,2BAA2B;AACpC,SAAS,4BAA4B;AACrC,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAClC,SAAS,4BAA4B;AACrC,SAAS,mCAAmC;AAC5C,SAAS,sCAAsC;AAC/C,SAAS,iCAAiC;AAE1C,IAAI,gBAAgB;AAGpB,IAAI,4BAAkD;AAY/C,SAAS,gBAAgB,MAAqB,UAA4B,CAAC,GAAG;AACnF,SAAO,SAAS,YAAkB;AAGhC,QAAI,iBAAiB,QAAQ,IAAI,aAAa,cAAe;AAC7D,oBAAgB;AAGhB,wBAAoB,KAAK,QAAQ;AACjC,yBAAqB,KAAK,aAAa,OAAO,CAAC,MAAkC,KAAK,IAAI,CAAC;AAG3F,oBAAgB,KAAK,OAAO;AAG5B,sBAAkB,KAAK,SAAS;AAGhC,QAAI,KAAK,sBAAsB;AAC7B,2BAAqB,KAAK,oBAAoB;AAAA,IAChD;AAGA,QAAI,KAAK,qBAAqB;AAC5B,kCAA4B,KAAK,mBAAmB;AAAA,IACtD;AAGA,QAAI,KAAK,wBAAwB;AAC/B,qCAA+B,KAAK,sBAAsB;AAAA,IAC5D;AAGA,QAAI,KAAK,iBAAiB;AACxB,gCAA0B,KAAK,eAAe;AAAA,IAChD;AAIA,gCAA4B,mCAAmC,MAAM,OAAO;AAC5E,SAAK;AAEL,YAAQ,yBAAyB;AAAA,EACnC;AACF;AAMA,eAAsB,2BAA0C;AAC9D,MAAI,2BAA2B;AAC7B,UAAM;AAAA,EACR;AACF;AAEA,eAAe,mCAAmC,MAAqB,SAA0C;AAE/G,MAAI;AACF,UAAM,CAAC,mBAAmB,mBAAmB,aAAa,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC9E,OAAO,mDAAmD;AAAA,MAC1D,OAAO,mDAAmD;AAAA,MAC1D,OAAO,kDAAkD;AAAA,IAC3D,CAAC;AAED,sBAAkB,yBAAyB,KAAK,sBAAsB;AACtE,sBAAkB,yBAAyB,KAAK,sBAAsB;AACtE,kBAAc,6BAA6B,KAAK,sBAAsB;AACtE,kBAAc,4BAA4B,KAAK,eAAe;AAAA,EAChE,QAAQ;AAAA,EAER;AASF;AAKO,SAAS,iBAA0B;AACxC,SAAO;AACT;AAKO,SAAS,sBAA4B;AAC1C,kBAAgB;AAClB;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,47 @@
1
+ const GLOBAL_ENRICHERS_KEY = "__openMercatoResponseEnrichers__";
2
+ let _enricherEntries = null;
3
+ function readGlobalEnrichers() {
4
+ try {
5
+ const value = globalThis[GLOBAL_ENRICHERS_KEY];
6
+ return Array.isArray(value) ? value : null;
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+ function writeGlobalEnrichers(entries) {
12
+ try {
13
+ ;
14
+ globalThis[GLOBAL_ENRICHERS_KEY] = entries;
15
+ } catch {
16
+ }
17
+ }
18
+ function registerResponseEnrichers(entries) {
19
+ const flat = [];
20
+ for (const entry of entries) {
21
+ for (const enricher of entry.enrichers) {
22
+ flat.push({ moduleId: entry.moduleId, enricher });
23
+ }
24
+ }
25
+ flat.sort((a, b) => (b.enricher.priority ?? 0) - (a.enricher.priority ?? 0));
26
+ _enricherEntries = flat;
27
+ writeGlobalEnrichers(flat);
28
+ }
29
+ function getResponseEnrichers() {
30
+ const globalEntries = readGlobalEnrichers();
31
+ if (globalEntries) return globalEntries;
32
+ if (!_enricherEntries) {
33
+ return [];
34
+ }
35
+ return _enricherEntries;
36
+ }
37
+ function getEnrichersForEntity(targetEntity) {
38
+ return getResponseEnrichers().filter(
39
+ (entry) => entry.enricher.targetEntity === targetEntity
40
+ );
41
+ }
42
+ export {
43
+ getEnrichersForEntity,
44
+ getResponseEnrichers,
45
+ registerResponseEnrichers
46
+ };
47
+ //# sourceMappingURL=enricher-registry.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/crud/enricher-registry.ts"],
4
+ "sourcesContent": ["/**\n * Response Enricher Registry\n *\n * Global registry for response enrichers using the same globalThis pattern\n * as injection widgets for HMR-safe storage.\n */\n\nimport type { EnricherRegistryEntry, ResponseEnricher } from './response-enricher'\n\nconst GLOBAL_ENRICHERS_KEY = '__openMercatoResponseEnrichers__'\n\nlet _enricherEntries: EnricherRegistryEntry[] | null = null\n\nfunction readGlobalEnrichers(): EnricherRegistryEntry[] | null {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_ENRICHERS_KEY]\n return Array.isArray(value) ? (value as EnricherRegistryEntry[]) : null\n } catch {\n return null\n }\n}\n\nfunction writeGlobalEnrichers(entries: EnricherRegistryEntry[]) {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_ENRICHERS_KEY] = entries\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Register response enrichers from all modules.\n * Called during bootstrap after generated enrichers are imported.\n */\nexport function registerResponseEnrichers(\n entries: Array<{ moduleId: string; enrichers: ResponseEnricher[] }>,\n) {\n const flat: EnricherRegistryEntry[] = []\n for (const entry of entries) {\n for (const enricher of entry.enrichers) {\n flat.push({ moduleId: entry.moduleId, enricher })\n }\n }\n flat.sort((a, b) => (b.enricher.priority ?? 0) - (a.enricher.priority ?? 0))\n _enricherEntries = flat\n writeGlobalEnrichers(flat)\n}\n\n/**\n * Get all registered response enrichers.\n */\nexport function getResponseEnrichers(): EnricherRegistryEntry[] {\n const globalEntries = readGlobalEnrichers()\n if (globalEntries) return globalEntries\n if (!_enricherEntries) {\n return []\n }\n return _enricherEntries\n}\n\n/**\n * Get enrichers targeting a specific entity, sorted by priority (higher first).\n */\nexport function getEnrichersForEntity(targetEntity: string): EnricherRegistryEntry[] {\n return getResponseEnrichers().filter(\n (entry) => entry.enricher.targetEntity === targetEntity,\n )\n}\n"],
5
+ "mappings": "AASA,MAAM,uBAAuB;AAE7B,IAAI,mBAAmD;AAEvD,SAAS,sBAAsD;AAC7D,MAAI;AACF,UAAM,QAAS,WAAuC,oBAAoB;AAC1E,WAAO,MAAM,QAAQ,KAAK,IAAK,QAAoC;AAAA,EACrE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,qBAAqB,SAAkC;AAC9D,MAAI;AACF;AAAC,IAAC,WAAuC,oBAAoB,IAAI;AAAA,EACnE,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,0BACd,SACA;AACA,QAAM,OAAgC,CAAC;AACvC,aAAW,SAAS,SAAS;AAC3B,eAAW,YAAY,MAAM,WAAW;AACtC,WAAK,KAAK,EAAE,UAAU,MAAM,UAAU,SAAS,CAAC;AAAA,IAClD;AAAA,EACF;AACA,OAAK,KAAK,CAAC,GAAG,OAAO,EAAE,SAAS,YAAY,MAAM,EAAE,SAAS,YAAY,EAAE;AAC3E,qBAAmB;AACnB,uBAAqB,IAAI;AAC3B;AAKO,SAAS,uBAAgD;AAC9D,QAAM,gBAAgB,oBAAoB;AAC1C,MAAI,cAAe,QAAO;AAC1B,MAAI,CAAC,kBAAkB;AACrB,WAAO,CAAC;AAAA,EACV;AACA,SAAO;AACT;AAKO,SAAS,sBAAsB,cAA+C;AACnF,SAAO,qBAAqB,EAAE;AAAA,IAC5B,CAAC,UAAU,MAAM,SAAS,iBAAiB;AAAA,EAC7C;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,242 @@
1
+ import { getEnrichersForEntity } from "./enricher-registry.js";
2
+ const DEFAULT_TIMEOUT = 2e3;
3
+ const SLOW_WARN_MS = 100;
4
+ const SLOW_ERROR_MS = 500;
5
+ const DEFAULT_CACHE_TTL_MS = 6e4;
6
+ function timeoutPromise(ms) {
7
+ return new Promise(
8
+ (_, reject) => setTimeout(() => reject(new Error(`Enricher timed out after ${ms}ms`)), ms)
9
+ );
10
+ }
11
+ function hasRequiredFeatures(enricher, userFeatures) {
12
+ if (!enricher.features || enricher.features.length === 0) return true;
13
+ if (!userFeatures) return false;
14
+ const hasFeature = (required) => {
15
+ for (const granted of userFeatures) {
16
+ if (granted === "*" || granted === required) return true;
17
+ if (granted.endsWith(".*")) {
18
+ const prefix = granted.slice(0, -1);
19
+ if (required.startsWith(prefix)) return true;
20
+ }
21
+ }
22
+ return false;
23
+ };
24
+ return enricher.features.every((feature) => hasFeature(feature));
25
+ }
26
+ function getActiveEnrichers(targetEntity, context) {
27
+ const entries = getEnrichersForEntity(targetEntity);
28
+ return entries.filter((entry) => {
29
+ const enricher = entry.enricher;
30
+ if (!hasRequiredFeatures(enricher, context.userFeatures)) return false;
31
+ if (enricher.disabledTenantIds?.includes(context.tenantId)) return false;
32
+ return true;
33
+ });
34
+ }
35
+ function resolveCache(context) {
36
+ const container = context.container;
37
+ if (!container?.resolve) return null;
38
+ try {
39
+ const cache = container.resolve("cache");
40
+ if (cache && typeof cache.get === "function" && typeof cache.set === "function") {
41
+ return cache;
42
+ }
43
+ } catch {
44
+ }
45
+ try {
46
+ const cacheService = container.resolve("cacheService");
47
+ if (cacheService && typeof cacheService.get === "function" && typeof cacheService.set === "function") {
48
+ return cacheService;
49
+ }
50
+ } catch {
51
+ }
52
+ return null;
53
+ }
54
+ function buildCacheKey(enricher, context, mode, recordIds) {
55
+ const sortedIds = [...recordIds].sort();
56
+ return `umes:enricher:${enricher.id}:tenant:${context.tenantId}:org:${context.organizationId}:mode:${mode}:ids:${JSON.stringify(sortedIds)}`;
57
+ }
58
+ function extractRecordId(record) {
59
+ const idValue = record.id;
60
+ if (typeof idValue === "string" && idValue.trim().length > 0) return idValue.trim();
61
+ if (typeof idValue === "number") return String(idValue);
62
+ return "unknown";
63
+ }
64
+ function getEnricherCacheTtl(enricher) {
65
+ const ttl = enricher.cache?.ttl;
66
+ if (typeof ttl === "number" && Number.isFinite(ttl) && ttl > 0) {
67
+ return ttl;
68
+ }
69
+ return DEFAULT_CACHE_TTL_MS;
70
+ }
71
+ function getEnricherCacheTags(enricher, context) {
72
+ const tags = /* @__PURE__ */ new Set([
73
+ `tenant:${context.tenantId}`,
74
+ `organization:${context.organizationId}`,
75
+ `enricher:${enricher.id}`
76
+ ]);
77
+ for (const tag of enricher.cache?.tags ?? []) {
78
+ if (!tag || tag.trim().length === 0) continue;
79
+ tags.add(tag);
80
+ }
81
+ return Array.from(tags);
82
+ }
83
+ async function readEnricherCache(cache, key) {
84
+ if (!cache) return null;
85
+ try {
86
+ const value = await cache.get(key);
87
+ return value == null ? null : value;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ async function writeEnricherCache(cache, key, value, ttl, tags) {
93
+ if (!cache) return;
94
+ try {
95
+ await cache.set(key, value, { ttl, tags });
96
+ } catch {
97
+ }
98
+ }
99
+ async function applyResponseEnrichers(items, targetEntity, context) {
100
+ const activeEntries = getActiveEnrichers(targetEntity, context);
101
+ if (activeEntries.length === 0) {
102
+ return { items, _meta: { enrichedBy: [] } };
103
+ }
104
+ const enrichedBy = [];
105
+ const enricherErrors = [];
106
+ let currentItems = items;
107
+ const cache = resolveCache(context);
108
+ for (const entry of activeEntries) {
109
+ const enricher = entry.enricher;
110
+ const timeout = enricher.timeout ?? DEFAULT_TIMEOUT;
111
+ const startTime = Date.now();
112
+ try {
113
+ let result;
114
+ const recordIds = currentItems.map((item) => extractRecordId(item));
115
+ const shouldUseCache = enricher.cache?.strategy === "read-through";
116
+ const cacheKey = shouldUseCache ? buildCacheKey(enricher, context, "many", recordIds) : null;
117
+ if (shouldUseCache && cacheKey) {
118
+ const cached = await readEnricherCache(cache, cacheKey);
119
+ if (cached) {
120
+ currentItems = cached;
121
+ enrichedBy.push(enricher.id);
122
+ continue;
123
+ }
124
+ }
125
+ if (enricher.enrichMany) {
126
+ result = await Promise.race([
127
+ enricher.enrichMany(currentItems, context),
128
+ timeoutPromise(timeout)
129
+ ]);
130
+ } else {
131
+ throw new Error(
132
+ `Enricher ${enricher.id} must implement enrichMany() for list endpoints`
133
+ );
134
+ }
135
+ const elapsedMs = Date.now() - startTime;
136
+ if (elapsedMs > SLOW_ERROR_MS) {
137
+ console.error(
138
+ `[UMES] Enricher ${enricher.id} took ${elapsedMs}ms (threshold: ${SLOW_ERROR_MS}ms)`
139
+ );
140
+ } else if (elapsedMs > SLOW_WARN_MS) {
141
+ console.warn(
142
+ `[UMES] Enricher ${enricher.id} took ${elapsedMs}ms (threshold: ${SLOW_WARN_MS}ms)`
143
+ );
144
+ }
145
+ currentItems = result;
146
+ if (shouldUseCache && cacheKey) {
147
+ await writeEnricherCache(
148
+ cache,
149
+ cacheKey,
150
+ result,
151
+ getEnricherCacheTtl(enricher),
152
+ getEnricherCacheTags(enricher, context)
153
+ );
154
+ }
155
+ enrichedBy.push(enricher.id);
156
+ } catch (err) {
157
+ if (enricher.critical) {
158
+ throw err;
159
+ }
160
+ const message = err instanceof Error ? err.message : String(err);
161
+ console.warn(`[UMES] Enricher ${enricher.id} failed: ${message}`);
162
+ enricherErrors.push(enricher.id);
163
+ if (enricher.fallback) {
164
+ currentItems = currentItems.map((item) => ({
165
+ ...item,
166
+ ...enricher.fallback
167
+ }));
168
+ }
169
+ }
170
+ }
171
+ return {
172
+ items: currentItems,
173
+ _meta: {
174
+ enrichedBy,
175
+ ...enricherErrors.length > 0 ? { enricherErrors } : {}
176
+ }
177
+ };
178
+ }
179
+ async function applyResponseEnricherToRecord(record, targetEntity, context) {
180
+ const activeEntries = getActiveEnrichers(targetEntity, context);
181
+ if (activeEntries.length === 0) {
182
+ return { record, _meta: { enrichedBy: [] } };
183
+ }
184
+ const enrichedBy = [];
185
+ const enricherErrors = [];
186
+ let currentRecord = record;
187
+ const cache = resolveCache(context);
188
+ for (const entry of activeEntries) {
189
+ const enricher = entry.enricher;
190
+ const timeout = enricher.timeout ?? DEFAULT_TIMEOUT;
191
+ try {
192
+ const recordId = extractRecordId(currentRecord);
193
+ const shouldUseCache = enricher.cache?.strategy === "read-through";
194
+ const cacheKey = shouldUseCache ? buildCacheKey(enricher, context, "one", [recordId]) : null;
195
+ if (shouldUseCache && cacheKey) {
196
+ const cached = await readEnricherCache(cache, cacheKey);
197
+ if (cached) {
198
+ currentRecord = cached;
199
+ enrichedBy.push(enricher.id);
200
+ continue;
201
+ }
202
+ }
203
+ const result = await Promise.race([
204
+ enricher.enrichOne(currentRecord, context),
205
+ timeoutPromise(timeout)
206
+ ]);
207
+ currentRecord = result;
208
+ if (shouldUseCache && cacheKey) {
209
+ await writeEnricherCache(
210
+ cache,
211
+ cacheKey,
212
+ result,
213
+ getEnricherCacheTtl(enricher),
214
+ getEnricherCacheTags(enricher, context)
215
+ );
216
+ }
217
+ enrichedBy.push(enricher.id);
218
+ } catch (err) {
219
+ if (enricher.critical) {
220
+ throw err;
221
+ }
222
+ const message = err instanceof Error ? err.message : String(err);
223
+ console.warn(`[UMES] Enricher ${enricher.id} failed: ${message}`);
224
+ enricherErrors.push(enricher.id);
225
+ if (enricher.fallback) {
226
+ currentRecord = { ...currentRecord, ...enricher.fallback };
227
+ }
228
+ }
229
+ }
230
+ return {
231
+ record: currentRecord,
232
+ _meta: {
233
+ enrichedBy,
234
+ ...enricherErrors.length > 0 ? { enricherErrors } : {}
235
+ }
236
+ };
237
+ }
238
+ export {
239
+ applyResponseEnricherToRecord,
240
+ applyResponseEnrichers
241
+ };
242
+ //# sourceMappingURL=enricher-runner.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/crud/enricher-runner.ts"],
4
+ "sourcesContent": ["/**\n * Response Enricher Runner\n *\n * Executes response enrichers against API response payloads.\n * Handles timeout, fallback, ACL feature gating, and error isolation.\n */\n\nimport type {\n EnricherContext,\n EnricherRegistryEntry,\n EnrichmentResult,\n ResponseEnricher,\n SingleEnrichmentResult,\n} from './response-enricher'\nimport { getEnrichersForEntity } from './enricher-registry'\n\nconst DEFAULT_TIMEOUT = 2000\nconst SLOW_WARN_MS = 100\nconst SLOW_ERROR_MS = 500\nconst DEFAULT_CACHE_TTL_MS = 60_000\n\nfunction timeoutPromise(ms: number): Promise<never> {\n return new Promise((_, reject) =>\n setTimeout(() => reject(new Error(`Enricher timed out after ${ms}ms`)), ms),\n )\n}\n\nfunction hasRequiredFeatures(\n enricher: ResponseEnricher,\n userFeatures: string[] | undefined,\n): boolean {\n if (!enricher.features || enricher.features.length === 0) return true\n if (!userFeatures) return false\n const hasFeature = (required: string): boolean => {\n for (const granted of userFeatures) {\n if (granted === '*' || granted === required) return true\n if (granted.endsWith('.*')) {\n const prefix = granted.slice(0, -1)\n if (required.startsWith(prefix)) return true\n }\n }\n return false\n }\n return enricher.features.every((feature) => hasFeature(feature))\n}\n\nfunction getActiveEnrichers(\n targetEntity: string,\n context: EnricherContext,\n): EnricherRegistryEntry[] {\n const entries = getEnrichersForEntity(targetEntity)\n return entries.filter((entry) => {\n const enricher = entry.enricher\n if (!hasRequiredFeatures(enricher, context.userFeatures)) return false\n if (enricher.disabledTenantIds?.includes(context.tenantId)) return false\n return true\n })\n}\n\ntype CacheLike = {\n get: (key: string) => Promise<unknown>\n set: (key: string, value: unknown, options?: { ttl?: number; tags?: string[] }) => Promise<unknown>\n}\n\nfunction resolveCache(context: EnricherContext): CacheLike | null {\n const container = context.container as { resolve?: (name: string) => unknown } | undefined\n if (!container?.resolve) return null\n try {\n const cache = container.resolve('cache') as CacheLike\n if (cache && typeof cache.get === 'function' && typeof cache.set === 'function') {\n return cache\n }\n } catch {\n // ignore cache resolution failures\n }\n try {\n const cacheService = container.resolve('cacheService') as CacheLike\n if (cacheService && typeof cacheService.get === 'function' && typeof cacheService.set === 'function') {\n return cacheService\n }\n } catch {\n // ignore cache service resolution failures\n }\n return null\n}\n\nfunction buildCacheKey(\n enricher: ResponseEnricher,\n context: EnricherContext,\n mode: 'one' | 'many',\n recordIds: string[],\n): string {\n const sortedIds = [...recordIds].sort()\n return `umes:enricher:${enricher.id}:tenant:${context.tenantId}:org:${context.organizationId}:mode:${mode}:ids:${JSON.stringify(sortedIds)}`\n}\n\nfunction extractRecordId(record: Record<string, unknown>): string {\n const idValue = record.id\n if (typeof idValue === 'string' && idValue.trim().length > 0) return idValue.trim()\n if (typeof idValue === 'number') return String(idValue)\n return 'unknown'\n}\n\nfunction getEnricherCacheTtl(enricher: ResponseEnricher): number {\n const ttl = enricher.cache?.ttl\n if (typeof ttl === 'number' && Number.isFinite(ttl) && ttl > 0) {\n return ttl\n }\n return DEFAULT_CACHE_TTL_MS\n}\n\nfunction getEnricherCacheTags(enricher: ResponseEnricher, context: EnricherContext): string[] {\n const tags = new Set<string>([\n `tenant:${context.tenantId}`,\n `organization:${context.organizationId}`,\n `enricher:${enricher.id}`,\n ])\n for (const tag of enricher.cache?.tags ?? []) {\n if (!tag || tag.trim().length === 0) continue\n tags.add(tag)\n }\n return Array.from(tags)\n}\n\nasync function readEnricherCache<T>(\n cache: CacheLike | null,\n key: string,\n): Promise<T | null> {\n if (!cache) return null\n try {\n const value = await cache.get(key)\n return value == null ? null : (value as T)\n } catch {\n return null\n }\n}\n\nasync function writeEnricherCache(\n cache: CacheLike | null,\n key: string,\n value: unknown,\n ttl: number,\n tags: string[],\n): Promise<void> {\n if (!cache) return\n try {\n await cache.set(key, value, { ttl, tags })\n } catch {\n // ignore cache write failures\n }\n}\n\n/**\n * Apply response enrichers to a list of records.\n *\n * Runs AFTER CrudHooks.afterList, BEFORE HTTP response serialization.\n * Each enricher runs independently \u2014 a failed non-critical enricher is skipped.\n */\nexport async function applyResponseEnrichers<T extends Record<string, unknown>>(\n items: T[],\n targetEntity: string,\n context: EnricherContext,\n): Promise<EnrichmentResult<T>> {\n const activeEntries = getActiveEnrichers(targetEntity, context)\n\n if (activeEntries.length === 0) {\n return { items, _meta: { enrichedBy: [] } }\n }\n\n const enrichedBy: string[] = []\n const enricherErrors: string[] = []\n let currentItems = items\n const cache = resolveCache(context)\n\n for (const entry of activeEntries) {\n const enricher = entry.enricher\n const timeout = enricher.timeout ?? DEFAULT_TIMEOUT\n const startTime = Date.now()\n\n try {\n let result: T[]\n const recordIds = currentItems.map((item) => extractRecordId(item))\n const shouldUseCache = enricher.cache?.strategy === 'read-through'\n const cacheKey = shouldUseCache ? buildCacheKey(enricher, context, 'many', recordIds) : null\n if (shouldUseCache && cacheKey) {\n const cached = await readEnricherCache<T[]>(cache, cacheKey)\n if (cached) {\n currentItems = cached\n enrichedBy.push(enricher.id)\n continue\n }\n }\n\n if (enricher.enrichMany) {\n result = await Promise.race([\n enricher.enrichMany(currentItems, context) as Promise<T[]>,\n timeoutPromise(timeout),\n ])\n } else {\n throw new Error(\n `Enricher ${enricher.id} must implement enrichMany() for list endpoints`,\n )\n }\n\n const elapsedMs = Date.now() - startTime\n if (elapsedMs > SLOW_ERROR_MS) {\n console.error(\n `[UMES] Enricher ${enricher.id} took ${elapsedMs}ms (threshold: ${SLOW_ERROR_MS}ms)`,\n )\n } else if (elapsedMs > SLOW_WARN_MS) {\n console.warn(\n `[UMES] Enricher ${enricher.id} took ${elapsedMs}ms (threshold: ${SLOW_WARN_MS}ms)`,\n )\n }\n\n currentItems = result\n if (shouldUseCache && cacheKey) {\n await writeEnricherCache(\n cache,\n cacheKey,\n result,\n getEnricherCacheTtl(enricher),\n getEnricherCacheTags(enricher, context),\n )\n }\n enrichedBy.push(enricher.id)\n } catch (err) {\n if (enricher.critical) {\n throw err\n }\n\n const message = err instanceof Error ? err.message : String(err)\n console.warn(`[UMES] Enricher ${enricher.id} failed: ${message}`)\n enricherErrors.push(enricher.id)\n\n if (enricher.fallback) {\n currentItems = currentItems.map((item) => ({\n ...item,\n ...enricher.fallback,\n })) as T[]\n }\n }\n }\n\n return {\n items: currentItems,\n _meta: {\n enrichedBy,\n ...(enricherErrors.length > 0 ? { enricherErrors } : {}),\n },\n }\n}\n\n/**\n * Apply response enrichers to a single record.\n *\n * Used for detail endpoints (GET /:id), POST, and PUT responses.\n */\nexport async function applyResponseEnricherToRecord<T extends Record<string, unknown>>(\n record: T,\n targetEntity: string,\n context: EnricherContext,\n): Promise<SingleEnrichmentResult<T>> {\n const activeEntries = getActiveEnrichers(targetEntity, context)\n\n if (activeEntries.length === 0) {\n return { record, _meta: { enrichedBy: [] } }\n }\n\n const enrichedBy: string[] = []\n const enricherErrors: string[] = []\n let currentRecord = record\n const cache = resolveCache(context)\n\n for (const entry of activeEntries) {\n const enricher = entry.enricher\n const timeout = enricher.timeout ?? DEFAULT_TIMEOUT\n\n try {\n const recordId = extractRecordId(currentRecord)\n const shouldUseCache = enricher.cache?.strategy === 'read-through'\n const cacheKey = shouldUseCache ? buildCacheKey(enricher, context, 'one', [recordId]) : null\n if (shouldUseCache && cacheKey) {\n const cached = await readEnricherCache<T>(cache, cacheKey)\n if (cached) {\n currentRecord = cached\n enrichedBy.push(enricher.id)\n continue\n }\n }\n const result = await Promise.race([\n enricher.enrichOne(currentRecord, context) as Promise<T>,\n timeoutPromise(timeout),\n ])\n\n currentRecord = result\n if (shouldUseCache && cacheKey) {\n await writeEnricherCache(\n cache,\n cacheKey,\n result,\n getEnricherCacheTtl(enricher),\n getEnricherCacheTags(enricher, context),\n )\n }\n enrichedBy.push(enricher.id)\n } catch (err) {\n if (enricher.critical) {\n throw err\n }\n\n const message = err instanceof Error ? err.message : String(err)\n console.warn(`[UMES] Enricher ${enricher.id} failed: ${message}`)\n enricherErrors.push(enricher.id)\n\n if (enricher.fallback) {\n currentRecord = { ...currentRecord, ...enricher.fallback } as T\n }\n }\n }\n\n return {\n record: currentRecord,\n _meta: {\n enrichedBy,\n ...(enricherErrors.length > 0 ? { enricherErrors } : {}),\n },\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -35,6 +35,7 @@ import {
35
35
  import { deriveCrudSegmentTag } from "./cache-stats.js";
36
36
  import { createProfiler, shouldEnableProfiler } from "@open-mercato/shared/lib/profiler";
37
37
  import { getTranslationOverlayPlugin } from "@open-mercato/shared/lib/localization/overlay-plugin";
38
+ import { applyResponseEnrichers, applyResponseEnricherToRecord } from "./enricher-runner.js";
38
39
  const DEFAULT_EXPORT_FORMATS = ["csv", "json", "xml", "markdown"];
39
40
  const DEFAULT_EXPORT_BATCH_SIZE = 1e3;
40
41
  const MIN_EXPORT_BATCH_SIZE = 100;
@@ -161,6 +162,8 @@ function normalizeFullRecordForExport(input) {
161
162
  const record = {};
162
163
  for (const [key, value] of Object.entries(input)) {
163
164
  if (key.startsWith("cf_") || key.startsWith("cf:")) continue;
165
+ if (key === "_meta") continue;
166
+ if (key.startsWith("_") && key.length > 1) continue;
164
167
  record[key] = value;
165
168
  }
166
169
  const custom = extractAllCustomFieldEntries(input);
@@ -515,6 +518,51 @@ function makeCrudRoute(opts) {
515
518
  return items;
516
519
  }
517
520
  };
521
+ async function buildEnricherContext(ctx) {
522
+ if (!opts.enrichers?.entityId) return null;
523
+ if (!ctx.auth) return null;
524
+ let userFeatures;
525
+ try {
526
+ const rbac = ctx.container.resolve("rbacService");
527
+ if (rbac?.getGrantedFeatures) {
528
+ userFeatures = await rbac.getGrantedFeatures(ctx.auth.sub, {
529
+ tenantId: ctx.auth.tenantId,
530
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId
531
+ });
532
+ }
533
+ } catch {
534
+ }
535
+ return {
536
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? "",
537
+ tenantId: ctx.auth.tenantId ?? "",
538
+ userId: ctx.auth.sub,
539
+ em: ctx.container.resolve("em"),
540
+ container: ctx.container,
541
+ userFeatures
542
+ };
543
+ }
544
+ async function enrichListPayload(payload, ctx, profiler) {
545
+ if (!opts.enrichers?.entityId) return;
546
+ const enricherCtx = await buildEnricherContext(ctx);
547
+ if (!enricherCtx) return;
548
+ profiler?.mark("enrichers_start");
549
+ const result = await applyResponseEnrichers(payload.items, opts.enrichers.entityId, enricherCtx);
550
+ payload.items = result.items;
551
+ if (result._meta.enrichedBy.length > 0 || result._meta.enricherErrors?.length) {
552
+ payload._meta = { ...payload._meta || {}, ...result._meta };
553
+ }
554
+ profiler?.mark("enrichers_complete", { enricherCount: result._meta.enrichedBy.length });
555
+ }
556
+ async function enrichSingleRecord(record, ctx) {
557
+ if (!opts.enrichers?.entityId) return record;
558
+ const enricherCtx = await buildEnricherContext(ctx);
559
+ if (!enricherCtx) return record;
560
+ const result = await applyResponseEnricherToRecord(record, opts.enrichers.entityId, enricherCtx);
561
+ if (result._meta.enrichedBy.length > 0 || result._meta.enricherErrors?.length) {
562
+ return { ...result.record, _meta: result._meta };
563
+ }
564
+ return result.record;
565
+ }
518
566
  async function ensureAuth(request) {
519
567
  const auth = request ? await getAuthFromRequest(request) : await getAuthFromCookies();
520
568
  if (!auth) return null;
@@ -719,6 +767,7 @@ function makeCrudRoute(opts) {
719
767
  query: validated
720
768
  });
721
769
  await opts.hooks?.afterList?.(payload2, { ...ctx, query: validated });
770
+ await enrichListPayload(payload2, ctx, profiler);
722
771
  logCacheOutcome("hit", items.length);
723
772
  const response2 = respondWithPayload(payload2);
724
773
  finishProfile({ result: "cache_hit", cacheStatus });
@@ -884,6 +933,7 @@ function makeCrudRoute(opts) {
884
933
  };
885
934
  await opts.hooks?.afterList?.(payload2, { ...ctx, query: validated });
886
935
  profiler.mark("after_list_hook");
936
+ await enrichListPayload(payload2, ctx, profiler);
887
937
  await maybeStoreCrudCache(payload2);
888
938
  profiler.mark("cache_store_attempt", { cacheEnabled });
889
939
  logCacheOutcome(cacheStatus, payload2.items.length);
@@ -997,6 +1047,7 @@ function makeCrudRoute(opts) {
997
1047
  const payload = { items: list, total: list.length };
998
1048
  await opts.hooks?.afterList?.(payload, { ...ctx, query: validated });
999
1049
  profiler.mark("after_list_hook");
1050
+ await enrichListPayload(payload, ctx, profiler);
1000
1051
  await maybeStoreCrudCache(payload);
1001
1052
  profiler.mark("cache_store_attempt", { cacheEnabled });
1002
1053
  logCacheOutcome(cacheStatus, payload.items.length);
@@ -1095,7 +1146,8 @@ function makeCrudRoute(opts) {
1095
1146
  });
1096
1147
  await de.flushOrmEntityChanges();
1097
1148
  await invalidateCrudCache(ctx.container, resourceKind, identifiers, ctx.auth.tenantId ?? null, "created", resourceTargets);
1098
- const payload = createConfig.response ? createConfig.response(entity) : { id: String(entity[ormCfg.idField]) };
1149
+ let payload = createConfig.response ? createConfig.response(entity) : { id: String(entity[ormCfg.idField]) };
1150
+ payload = await enrichSingleRecord(payload, ctx);
1099
1151
  return json(payload, { status: 201 });
1100
1152
  } catch (e) {
1101
1153
  return handleError(e);