@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.
- package/dist/lib/bootstrap/factory.js +4 -0
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/crud/enricher-registry.js +47 -0
- package/dist/lib/crud/enricher-registry.js.map +7 -0
- package/dist/lib/crud/enricher-runner.js +242 -0
- package/dist/lib/crud/enricher-runner.js.map +7 -0
- package/dist/lib/crud/factory.js +53 -1
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/response-enricher.js +1 -0
- package/dist/lib/crud/response-enricher.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/events/factory.js +5 -0
- package/dist/modules/events/factory.js.map +2 -2
- package/dist/modules/registry.js.map +1 -1
- package/dist/modules/widgets/injection-loader.js +100 -40
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/dist/modules/widgets/injection-position.js +48 -0
- package/dist/modules/widgets/injection-position.js.map +7 -0
- package/dist/modules/widgets/injection-progress.js +1 -0
- package/dist/modules/widgets/injection-progress.js.map +7 -0
- package/package.json +1 -1
- package/src/lib/bootstrap/factory.ts +6 -0
- package/src/lib/bootstrap/types.ts +6 -0
- package/src/lib/crud/enricher-registry.ts +68 -0
- package/src/lib/crud/enricher-runner.ts +329 -0
- package/src/lib/crud/factory.ts +79 -1
- package/src/lib/crud/response-enricher.ts +110 -0
- package/src/modules/events/factory.ts +9 -0
- package/src/modules/events/types.ts +2 -0
- package/src/modules/registry.ts +2 -2
- package/src/modules/widgets/__tests__/injection-position.test.ts +33 -0
- package/src/modules/widgets/injection-loader.ts +140 -50
- package/src/modules/widgets/injection-position.ts +59 -0
- package/src/modules/widgets/injection-progress.ts +35 -0
- 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;
|
|
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
|
+
}
|
package/dist/lib/crud/factory.js
CHANGED
|
@@ -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
|
-
|
|
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);
|