@open-mercato/shared 0.6.3-develop.3876.1.d40fe4ec2d → 0.6.3-develop.3881.1.0b590ac4eb

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.
@@ -232,12 +232,68 @@ function selectDefinitionForRecord(defs, organizationId, tenantId) {
232
232
  if (global.length) return sortDefinitionSummaries(global)[0];
233
233
  return sortDefinitionSummaries(defs)[0] ?? null;
234
234
  }
235
- async function loadCustomFieldDefinitionIndex(opts) {
236
- const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds];
237
- const entityIds = list.map((id) => typeof id === "string" ? id.trim() : String(id ?? "")).filter((id) => id.length > 0);
238
- if (!entityIds.length) return /* @__PURE__ */ new Map();
235
+ const CF_DEF_INDEX_CACHE_KEY_PREFIX = "crud:cf-def-index";
236
+ const CF_DEF_INDEX_DEFAULT_TTL_MS = 0;
237
+ function resolveCfDefIndexCacheTtlMs() {
238
+ const raw = process.env.OM_CF_DEF_CACHE_TTL_MS;
239
+ if (raw === void 0) return CF_DEF_INDEX_DEFAULT_TTL_MS;
240
+ const parsed = Number(raw);
241
+ if (!Number.isFinite(parsed) || parsed < 0) return CF_DEF_INDEX_DEFAULT_TTL_MS;
242
+ return parsed;
243
+ }
244
+ function buildCfDefIndexCacheKey(opts) {
245
+ const tenant = opts.tenantId ?? "global";
246
+ const entities = opts.entityIds.slice().sort().join("|");
247
+ const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort().join("|") : "none";
248
+ const fieldset = opts.fieldsetKey ?? "all";
249
+ return `${CF_DEF_INDEX_CACHE_KEY_PREFIX}:${tenant}:${entities}:${orgs}:${fieldset}`;
250
+ }
251
+ function buildCfDefIndexCacheTags(opts) {
252
+ const tenant = opts.tenantId ?? "global";
253
+ const tagBase = `entities:definitions:${tenant}`;
254
+ const tags = /* @__PURE__ */ new Set([tagBase]);
255
+ for (const entityId of opts.entityIds) {
256
+ tags.add(`${tagBase}:entity:${entityId}`);
257
+ }
258
+ return Array.from(tags);
259
+ }
260
+ function normalizeFieldsetKey(value) {
261
+ if (!value) return null;
262
+ if (Array.isArray(value)) {
263
+ const cleaned = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter((entry) => entry.length > 0);
264
+ if (!cleaned.length) return null;
265
+ return cleaned.sort().join(",");
266
+ }
267
+ if (typeof value !== "string") return null;
268
+ const trimmed = value.trim();
269
+ return trimmed.length > 0 ? trimmed : null;
270
+ }
271
+ function serializableIndexFromMap(index) {
272
+ return Array.from(index.entries());
273
+ }
274
+ function indexMapFromSerializable(value) {
275
+ if (!Array.isArray(value)) return null;
276
+ const map = /* @__PURE__ */ new Map();
277
+ for (const entry of value) {
278
+ if (!Array.isArray(entry) || entry.length !== 2) return null;
279
+ const [key, summaries] = entry;
280
+ if (typeof key !== "string" || !Array.isArray(summaries)) return null;
281
+ map.set(key, summaries);
282
+ }
283
+ return map;
284
+ }
285
+ const requestScopedCfDefIndexCache = /* @__PURE__ */ new WeakMap();
286
+ function getRequestScopedCfDefIndexCache(scope) {
287
+ let bucket = requestScopedCfDefIndexCache.get(scope);
288
+ if (!bucket) {
289
+ bucket = /* @__PURE__ */ new Map();
290
+ requestScopedCfDefIndexCache.set(scope, bucket);
291
+ }
292
+ return bucket;
293
+ }
294
+ async function loadCustomFieldDefinitionIndexFresh(opts) {
295
+ const { em, entityIds, orgCandidates } = opts;
239
296
  const tenantId = opts.tenantId ?? null;
240
- const orgCandidates = Array.isArray(opts.organizationIds) ? opts.organizationIds.map((id) => typeof id === "string" ? id.trim() : id).filter((id) => typeof id === "string" && id.length > 0) : [];
241
297
  const scopeClauses = [
242
298
  tenantId ? { $or: [{ tenantId }, { tenantId: null }] } : { tenantId: null }
243
299
  ];
@@ -254,7 +310,7 @@ async function loadCustomFieldDefinitionIndex(opts) {
254
310
  isActive: true,
255
311
  $and: scopeClauses
256
312
  };
257
- const defs = await opts.em.find(CustomFieldDef, where);
313
+ const defs = await em.find(CustomFieldDef, where);
258
314
  const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset);
259
315
  const index = /* @__PURE__ */ new Map();
260
316
  defs.forEach((def) => {
@@ -277,6 +333,56 @@ async function loadCustomFieldDefinitionIndex(opts) {
277
333
  });
278
334
  return index;
279
335
  }
336
+ async function loadCustomFieldDefinitionIndex(opts) {
337
+ const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds];
338
+ const entityIds = list.map((id) => typeof id === "string" ? id.trim() : String(id ?? "")).filter((id) => id.length > 0);
339
+ if (!entityIds.length) return /* @__PURE__ */ new Map();
340
+ const tenantId = opts.tenantId ?? null;
341
+ const orgCandidates = Array.isArray(opts.organizationIds) ? opts.organizationIds.map((id) => typeof id === "string" ? id.trim() : id).filter((id) => typeof id === "string" && id.length > 0) : [];
342
+ const fieldsetKey = normalizeFieldsetKey(opts.fieldset);
343
+ const ttlMs = resolveCfDefIndexCacheTtlMs();
344
+ const cacheKey = buildCfDefIndexCacheKey({
345
+ tenantId,
346
+ entityIds,
347
+ organizationIds: orgCandidates,
348
+ fieldsetKey
349
+ });
350
+ const requestBucket = opts.requestScope ? getRequestScopedCfDefIndexCache(opts.requestScope) : null;
351
+ if (requestBucket) {
352
+ const cached = requestBucket.get(cacheKey);
353
+ if (cached) return cached;
354
+ }
355
+ const sharedCache = ttlMs > 0 ? opts.cache ?? null : null;
356
+ if (sharedCache && typeof sharedCache.get === "function") {
357
+ try {
358
+ const cached = await sharedCache.get(cacheKey);
359
+ const restored = indexMapFromSerializable(cached);
360
+ if (restored) {
361
+ if (requestBucket) requestBucket.set(cacheKey, restored);
362
+ return restored;
363
+ }
364
+ } catch (err) {
365
+ console.warn("[crud:cf-def-cache] read failed", err);
366
+ }
367
+ }
368
+ const index = await loadCustomFieldDefinitionIndexFresh({
369
+ ...opts,
370
+ entityIds,
371
+ orgCandidates
372
+ });
373
+ if (sharedCache && typeof sharedCache.set === "function") {
374
+ try {
375
+ await sharedCache.set(cacheKey, serializableIndexFromMap(index), {
376
+ ttl: ttlMs,
377
+ tags: buildCfDefIndexCacheTags({ tenantId, entityIds })
378
+ });
379
+ } catch (err) {
380
+ console.warn("[crud:cf-def-cache] write failed", err);
381
+ }
382
+ }
383
+ if (requestBucket) requestBucket.set(cacheKey, index);
384
+ return index;
385
+ }
280
386
  function applyCustomFieldsNormalization(record, decorated, options = {}) {
281
387
  const stripPrefixedKeys = options.stripPrefixedKeys === true;
282
388
  const base = {};
@@ -490,6 +596,7 @@ export {
490
596
  extractAllCustomFieldEntries,
491
597
  extractCustomFieldValuesFromPayload,
492
598
  extractCustomFieldsFromItem,
599
+ getRequestScopedCfDefIndexCache,
493
600
  loadCustomFieldDefinitionIndex,
494
601
  loadCustomFieldValues,
495
602
  normalizeCustomFieldValue,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/custom-fields.ts"],
4
- "sourcesContent": ["import type { CustomFieldSet, EntityId } from '@open-mercato/shared/modules/entities'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { CustomFieldDef, CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'\nimport type { WhereValue } from '@open-mercato/shared/lib/query/types'\nimport type { TenantDataEncryptionService } from '../encryption/tenantDataEncryptionService'\nimport { decryptCustomFieldValue, resolveTenantEncryptionService } from '../encryption/customFieldValues'\nimport { parseBooleanToken } from '../boolean'\nimport { extractCustomFieldEntries } from './custom-fields-client'\n\nexport type CustomFieldSelectors = {\n keys: string[]\n selectors: string[] // e.g. ['cf:priority', 'cf:severity']\n outputKeys: string[] // e.g. ['cf_priority', 'cf_severity']\n}\n\nexport type SplitCustomFieldPayload = {\n base: Record<string, unknown>\n custom: Record<string, unknown>\n}\n\nexport type CustomFieldDefinitionSummary = {\n key: string\n label: string | null\n kind: string | null\n multi: boolean\n dictionaryId?: string | null\n organizationId?: string | null\n tenantId?: string | null\n priority: number\n updatedAt: number\n}\n\nexport type CustomFieldDefinitionIndex = Map<string, CustomFieldDefinitionSummary[]>\n\nexport type CustomFieldDisplayEntry = {\n key: string\n label: string | null\n value: unknown\n kind: string | null\n multi: boolean\n}\n\nexport type CustomFieldDisplayPayload = {\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport type CustomFieldSnapshot = {\n entries: Record<string, unknown>\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport function buildCustomFieldSelectorsForEntity(entityId: EntityId, sets: CustomFieldSet[]): CustomFieldSelectors {\n const keys = Array.from(new Set(\n (sets || [])\n .filter((s) => s.entity === entityId)\n .flatMap((s) => (s.fields || []).map((f) => f.key))\n ))\n const selectors = keys.map((k) => `cf:${k}`)\n const outputKeys = keys.map((k) => `cf_${k}`)\n return { keys, selectors, outputKeys }\n}\n\nexport function normalizeCustomFieldValue(val: unknown): unknown {\n if (Array.isArray(val)) return val\n if (typeof val === 'string') {\n const s = val.trim()\n // Parse Postgres array-like '{a,b,c}' to string[] when present\n if (s.startsWith('{') && s.endsWith('}')) {\n const inner = s.slice(1, -1).trim()\n if (!inner) return []\n return inner.split(/[\\s,]+/).map((x) => x.trim()).filter(Boolean)\n }\n return s\n }\n return val as any\n}\n\n// Extracts cf_* fields from a record that may contain both 'cf:<key>' and/or 'cf_<key>'\nexport function extractCustomFieldsFromItem(item: Record<string, unknown>, keys: string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of keys) {\n const colon = item[`cf:${key}` as keyof typeof item]\n const snake = item[`cf_${key}` as keyof typeof item]\n const value = colon !== undefined ? colon : snake\n if (value !== undefined) out[`cf_${key}`] = normalizeCustomFieldValue(value)\n }\n return out\n}\n\nexport function extractAllCustomFieldEntries(item: Record<string, unknown>): Record<string, unknown> {\n return extractCustomFieldEntries(item)\n}\n\nfunction normalizeFieldsetFilter(input?: string | string[] | null): Set<string | null> | null {\n if (input == null) return null\n const values = Array.isArray(input) ? input : [input]\n const normalized = new Set<string | null>()\n for (const raw of values) {\n if (raw == null) continue\n const trimmed = String(raw).trim()\n if (!trimmed) {\n normalized.add(null)\n } else {\n normalized.add(trimmed)\n }\n }\n return normalized.size ? normalized : null\n}\n\nexport async function buildCustomFieldFiltersFromQuery(opts: {\n entityId?: EntityId\n entityIds?: EntityId[]\n query: Record<string, unknown>\n em: EntityManager\n tenantId: string | null | undefined\n fieldset?: string | string[] | null\n}): Promise<Record<string, WhereValue>> {\n const out: Record<string, WhereValue> = {}\n const entries = Object.entries(opts.query).filter(([k]) => k.startsWith('cf_'))\n if (!entries.length) return out\n\n const entityIdList = Array.isArray(opts.entityIds) && opts.entityIds.length\n ? opts.entityIds\n : opts.entityId\n ? [opts.entityId]\n : []\n if (!entityIdList.length) return out\n\n // Tenant-only scope: allow global (null) or tenant match; ignore organization here\n const defs = await opts.em.find(CustomFieldDef, {\n entityId: { $in: entityIdList as any },\n isActive: true,\n $and: [\n { $or: [ { tenantId: opts.tenantId as any }, { tenantId: null } ] },\n ],\n })\n const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)\n const order = new Map<string, number>()\n entityIdList.map(String).forEach((id, index) => order.set(id, index))\n const byKey: Record<string, { kind: string; multi?: boolean; entityId: string }> = {}\n for (const d of defs) {\n if (fieldsetFilter) {\n const fieldsets = Array.isArray(d.configJson?.fieldsets)\n ? d.configJson.fieldsets\n .filter((entry: unknown): entry is string => typeof entry === 'string')\n .map((entry: string) => entry.trim())\n .filter((entry: string) => entry.length > 0)\n : []\n const rawFieldset = typeof d.configJson?.fieldset === 'string' ? d.configJson.fieldset.trim() : ''\n const normalizedFieldset = rawFieldset.length ? rawFieldset : null\n const matches = fieldsets.length > 0\n ? fieldsets.some((entry: string) => fieldsetFilter.has(entry))\n : fieldsetFilter.has(normalizedFieldset)\n if (!matches) continue\n }\n const key = d.key\n const entityId = String(d.entityId)\n const current = byKey[key]\n const rankNew = order.get(entityId) ?? Number.MAX_SAFE_INTEGER\n if (!current) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n continue\n }\n const rankOld = order.get(current.entityId) ?? Number.MAX_SAFE_INTEGER\n if (rankNew < rankOld) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n }\n }\n\n const coerce = (kind: string, v: unknown) => {\n if (v == null) return v as undefined\n switch (kind) {\n case 'integer': return Number.parseInt(String(v), 10)\n case 'float': return Number.parseFloat(String(v))\n case 'boolean': return parseBooleanToken(String(v)) === true\n case 'date':\n case 'datetime': return String(v)\n default: return String(v)\n }\n }\n\n for (const [rawKey, rawVal] of entries) {\n const isIn = rawKey.endsWith('In')\n const key = isIn ? rawKey.replace(/^cf_/, '').replace(/In$/, '') : rawKey.replace(/^cf_/, '')\n const def = byKey[key]\n const fieldId = `cf:${key}`\n if (!def) continue\n if (isIn) {\n const list = Array.isArray(rawVal)\n ? (rawVal as unknown[])\n : String(rawVal)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (list.length) out[fieldId] = { $in: list.map((x) => coerce(def.kind, x)) as (string[] | number[] | boolean[]) }\n } else {\n out[fieldId] = coerce(def.kind, rawVal)\n }\n }\n\n return out\n}\n\nexport function splitCustomFieldPayload(raw: unknown): SplitCustomFieldPayload {\n const base: Record<string, unknown> = {}\n const custom: Record<string, unknown> = {}\n if (!raw || typeof raw !== 'object') return { base, custom }\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (key === 'customFields') {\n if (Array.isArray(value)) {\n value.forEach((entry) => {\n if (!entry || typeof entry !== 'object') return\n const entryKey = typeof (entry as any).key === 'string' ? (entry as any).key.trim() : ''\n if (!entryKey) return\n custom[entryKey] = (entry as any).value\n })\n continue\n }\n if (value && typeof value === 'object') {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n const normalizedKey = typeof ck === 'string' ? ck.trim() : ''\n if (!normalizedKey) continue\n custom[normalizedKey] = cv\n }\n continue\n }\n }\n if (key === 'customValues' && value && typeof value === 'object' && !Array.isArray(value)) {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n custom[String(ck)] = cv\n }\n continue\n }\n if (key.startsWith('cf_')) {\n custom[key.slice(3)] = value\n continue\n }\n if (key.startsWith('cf:')) {\n custom[key.slice(3)] = value\n continue\n }\n base[key] = value\n }\n return { base, custom }\n}\n\nexport function extractCustomFieldValuesFromPayload(raw: Record<string, unknown>): Record<string, unknown> {\n return splitCustomFieldPayload(raw).custom\n}\n\nfunction normalizeDefinitionKey(key: unknown): string {\n if (typeof key !== 'string') return ''\n const trimmed = key.trim()\n return trimmed.length ? trimmed.toLowerCase() : ''\n}\n\nfunction normalizeDefinitionConfig(raw: unknown): Record<string, any> {\n if (!raw) return {}\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return { ...(parsed as Record<string, any>) }\n }\n return {}\n } catch {\n return {}\n }\n }\n if (typeof raw === 'object' && !Array.isArray(raw)) {\n return { ...(raw as Record<string, any>) }\n }\n return {}\n}\n\nfunction summarizeDefinition(def: CustomFieldDef): CustomFieldDefinitionSummary | null {\n const normalizedKey = normalizeDefinitionKey(def.key)\n if (!normalizedKey) return null\n const cfg = normalizeDefinitionConfig((def as any).configJson)\n const label =\n typeof cfg.label === 'string' && cfg.label.trim().length\n ? cfg.label.trim()\n : def.key\n const dictionaryId =\n typeof cfg.dictionaryId === 'string' && cfg.dictionaryId.trim().length\n ? cfg.dictionaryId.trim()\n : null\n const multi =\n cfg.multi !== undefined ? Boolean(cfg.multi) : false\n const priority =\n typeof cfg.priority === 'number' ? cfg.priority : 0\n const updatedAt =\n def.updatedAt instanceof Date\n ? def.updatedAt.getTime()\n : new Date(def.updatedAt as any).getTime()\n return {\n key: def.key,\n label,\n kind: typeof def.kind === 'string' ? def.kind : null,\n multi,\n dictionaryId,\n organizationId: def.organizationId ?? null,\n tenantId: def.tenantId ?? null,\n priority,\n updatedAt: Number.isNaN(updatedAt) ? 0 : updatedAt,\n }\n}\n\nfunction sortDefinitionSummaries(defs: CustomFieldDefinitionSummary[]): CustomFieldDefinitionSummary[] {\n return [...defs].sort((a, b) => {\n const priorityDiff = (a.priority ?? 0) - (b.priority ?? 0)\n if (priorityDiff !== 0) return priorityDiff\n const updatedDiff = (b.updatedAt ?? 0) - (a.updatedAt ?? 0)\n if (updatedDiff !== 0) return updatedDiff\n return a.key.localeCompare(b.key)\n })\n}\n\nfunction selectDefinitionForRecord(\n defs: CustomFieldDefinitionSummary[],\n organizationId: string | null,\n tenantId: string | null,\n): CustomFieldDefinitionSummary | null {\n if (!defs.length) return null\n const prioritizedForOrg = defs.filter(\n (def) => def.organizationId && organizationId && def.organizationId === organizationId,\n )\n if (prioritizedForOrg.length) return sortDefinitionSummaries(prioritizedForOrg)[0]\n const prioritizedForTenant = defs.filter(\n (def) => def.tenantId && tenantId && def.tenantId === tenantId && !def.organizationId,\n )\n if (prioritizedForTenant.length) return sortDefinitionSummaries(prioritizedForTenant)[0]\n const global = defs.filter((def) => !def.organizationId)\n if (global.length) return sortDefinitionSummaries(global)[0]\n return sortDefinitionSummaries(defs)[0] ?? null\n}\n\nexport async function loadCustomFieldDefinitionIndex(opts: {\n em: EntityManager\n entityIds: string | string[]\n tenantId?: string | null | undefined\n organizationIds?: Array<string | null | undefined> | null\n fieldset?: string | string[] | null\n}): Promise<CustomFieldDefinitionIndex> {\n const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds]\n const entityIds = list\n .map((id) => (typeof id === 'string' ? id.trim() : String(id ?? '')))\n .filter((id) => id.length > 0)\n if (!entityIds.length) return new Map()\n const tenantId = opts.tenantId ?? null\n const orgCandidates = Array.isArray(opts.organizationIds)\n ? opts.organizationIds\n .map((id) => (typeof id === 'string' ? id.trim() : id))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n : []\n const scopeClauses: Record<string, unknown>[] = [\n tenantId\n ? { $or: [{ tenantId: tenantId as any }, { tenantId: null }] }\n : { tenantId: null },\n ]\n if (orgCandidates.length) {\n scopeClauses.push({\n $or: [{ organizationId: { $in: orgCandidates as any } }, { organizationId: null }],\n })\n } else {\n scopeClauses.push({ organizationId: null })\n }\n const where: Record<string, unknown> = {\n entityId: { $in: entityIds as any },\n deletedAt: null,\n isActive: true,\n $and: scopeClauses,\n }\n const defs = await opts.em.find(CustomFieldDef, where as any)\n const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)\n const index: CustomFieldDefinitionIndex = new Map()\n defs.forEach((def) => {\n if (fieldsetFilter) {\n const config = normalizeDefinitionConfig((def as any).configJson)\n const fieldsets = Array.isArray(config.fieldsets)\n ? config.fieldsets\n .filter((entry: unknown): entry is string => typeof entry === 'string')\n .map((entry: string) => entry.trim())\n .filter((entry: string) => entry.length > 0)\n : []\n const fieldset = typeof config.fieldset === 'string' && config.fieldset.trim().length > 0\n ? config.fieldset.trim()\n : null\n const matches = fieldsets.length > 0\n ? fieldsets.some((entry: string) => fieldsetFilter.has(entry))\n : fieldsetFilter.has(fieldset)\n if (!matches) return\n }\n const summary = summarizeDefinition(def)\n if (!summary) return\n const normalizedKey = normalizeDefinitionKey(summary.key)\n if (!normalizedKey) return\n if (!index.has(normalizedKey)) index.set(normalizedKey, [])\n index.get(normalizedKey)!.push(summary)\n })\n index.forEach((entries, key) => {\n index.set(key, sortDefinitionSummaries(entries))\n })\n return index\n}\n\nexport type ApplyCustomFieldsNormalizationOptions = {\n /**\n * When true, removes raw `cf_*` and `cf:*` keys from the record once they\n * have been extracted into `customValues` / `customFields`. Produces a single\n * canonical response shape (issue #1769). Defaults to `false` to preserve the\n * existing wire format for callers that read `cf_*` from the top level.\n */\n stripPrefixedKeys?: boolean\n}\n\nexport function applyCustomFieldsNormalization(\n record: Record<string, unknown>,\n decorated: CustomFieldDisplayPayload,\n options: ApplyCustomFieldsNormalizationOptions = {},\n): Record<string, unknown> {\n const stripPrefixedKeys = options.stripPrefixedKeys === true\n const base: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (stripPrefixedKeys && (key.startsWith('cf_') || key.startsWith('cf:'))) continue\n base[key] = value\n }\n base.customValues = decorated.customValues\n base.customFields = decorated.customFields\n return base\n}\n\nexport function decorateRecordWithCustomFields(\n record: Record<string, unknown>,\n definitions: CustomFieldDefinitionIndex,\n context: {\n organizationId?: string | null\n tenantId?: string | null\n } = {},\n): CustomFieldDisplayPayload {\n const rawEntries = extractAllCustomFieldEntries(record)\n if (!Object.keys(rawEntries).length) {\n return { customValues: null, customFields: [] }\n }\n const values: Record<string, unknown> = {}\n const entries: Array<{ entry: CustomFieldDisplayEntry; priority: number; updatedAt: number }> = []\n const organizationId = context.organizationId ?? null\n const tenantId = context.tenantId ?? null\n\n Object.entries(rawEntries).forEach(([prefixedKey, value]) => {\n const bareKey = prefixedKey.replace(/^cf_/, '')\n const normalizedKey = normalizeDefinitionKey(bareKey)\n if (!normalizedKey) return\n const defsForKey = definitions.get(normalizedKey) ?? []\n const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId)\n // Skip custom field values without active definitions to prevent orphaned fields\n if (!resolvedDef) return\n values[bareKey] = value\n const entry: CustomFieldDisplayEntry = {\n key: bareKey,\n label: resolvedDef.label ?? bareKey,\n value,\n kind: resolvedDef.kind ?? null,\n multi: resolvedDef.multi ?? Array.isArray(value),\n }\n entries.push({\n entry,\n priority: resolvedDef.priority ?? Number.MAX_SAFE_INTEGER,\n updatedAt: resolvedDef.updatedAt ?? 0,\n })\n })\n\n const ordered = entries\n .sort((a, b) => {\n const priorityDiff = a.priority - b.priority\n if (priorityDiff !== 0) return priorityDiff\n const updatedDiff = b.updatedAt - a.updatedAt\n if (updatedDiff !== 0) return updatedDiff\n return a.entry.key.localeCompare(b.entry.key)\n })\n .map((item) => item.entry)\n\n return {\n customValues: Object.keys(values).length ? values : null,\n customFields: ordered,\n }\n}\n\nexport async function loadCustomFieldValues(opts: {\n em: EntityManager\n entityId: EntityId\n recordIds: string[]\n tenantIdByRecord?: Record<string, string | null | undefined>\n organizationIdByRecord?: Record<string, string | null | undefined>\n tenantFallbacks?: (string | null | undefined)[]\n encryptionService?: TenantDataEncryptionService | null\n}): Promise<Record<string, Record<string, unknown>>> {\n const { em, entityId, recordIds } = opts\n if (!Array.isArray(recordIds) || recordIds.length === 0) return {}\n\n const normalizedRecordIds = recordIds.map((id) => String(id))\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em, opts.encryptionService)\n return encryptionService\n }\n const tenantCandidates = new Set<string | null>()\n tenantCandidates.add(null)\n if (opts.tenantIdByRecord) {\n for (const val of Object.values(opts.tenantIdByRecord)) {\n tenantCandidates.add(val ? String(val) : null)\n }\n }\n if (opts.tenantFallbacks) {\n for (const val of opts.tenantFallbacks) tenantCandidates.add(val ? String(val) : null)\n }\n const fallbackTenant = (opts.tenantFallbacks || []).find((t) => t != null) ?? null\n\n const tenantList = Array.from(tenantCandidates)\n const tenantNonNull = tenantList.filter((t): t is string => t !== null)\n const tenantFilter = tenantNonNull.length\n ? { tenantId: { $in: [...tenantNonNull, null] as any } }\n : { tenantId: null }\n const cfRows = await em.find(CustomFieldValue, {\n entityId: entityId as any,\n recordId: { $in: normalizedRecordIds as any },\n deletedAt: null,\n ...(tenantList.length ? tenantFilter : {}),\n })\n\n if (!cfRows.length) return {}\n\n const allKeys = Array.from(new Set(cfRows.map((row) => String(row.fieldKey))))\n const organizationCandidates = new Set<string | null>()\n organizationCandidates.add(null)\n if (opts.organizationIdByRecord) {\n for (const val of Object.values(opts.organizationIdByRecord)) {\n organizationCandidates.add(val ? String(val) : null)\n }\n }\n for (const row of cfRows) {\n organizationCandidates.add(row.organizationId ? String(row.organizationId) : null)\n }\n const orgList = Array.from(organizationCandidates)\n\n const defs = allKeys.length\n ? await em.find(CustomFieldDef, {\n entityId: entityId as any,\n key: { $in: allKeys as any },\n deletedAt: null,\n isActive: true,\n ...(tenantList.length ? { tenantId: tenantFilter.tenantId } : {}),\n organizationId: { $in: orgList as any },\n })\n : []\n\n const defsByKey = new Map<string, CustomFieldDef[]>()\n for (const def of defs) {\n const list = defsByKey.get(def.key) || []\n list.push(def)\n defsByKey.set(def.key, list)\n }\n\n const pickDefinition = (fieldKey: string, organizationId: string | null, tenantId: string | null) => {\n const candidates = defsByKey.get(fieldKey)\n if (!candidates || candidates.length === 0) return null\n const active = candidates.filter((opt) => opt.isActive !== false && !opt.deletedAt)\n const list = active.length ? active : candidates\n if (organizationId && tenantId) {\n const exact = list.find((opt) => opt.organizationId === organizationId && opt.tenantId === tenantId)\n if (exact) return exact\n }\n if (organizationId) {\n const orgMatch = list.find((opt) => opt.organizationId === organizationId && (!tenantId || opt.tenantId == null || opt.tenantId === tenantId))\n if (orgMatch) return orgMatch\n }\n if (tenantId) {\n const tenantMatch = list.find((opt) => opt.organizationId == null && opt.tenantId === tenantId)\n if (tenantMatch) return tenantMatch\n }\n const global = list.find((opt) => opt.organizationId == null && opt.tenantId == null)\n return global ?? list[0]\n }\n\n const valueFromRow = (row: CustomFieldValue): unknown => {\n if (row.valueMultiline !== null && row.valueMultiline !== undefined) return row.valueMultiline\n if (row.valueText !== null && row.valueText !== undefined) return row.valueText\n if (row.valueInt !== null && row.valueInt !== undefined) return row.valueInt\n if (row.valueFloat !== null && row.valueFloat !== undefined) return row.valueFloat\n if (row.valueBool !== null && row.valueBool !== undefined) return row.valueBool\n return null\n }\n\n type Bucket = { orgId: string | null; tenantId: string | null; values: unknown[]; def?: CustomFieldDef | null; encrypted?: boolean }\n const buckets = new Map<string, Bucket>()\n\n for (const row of cfRows) {\n const recordId = String(row.recordId)\n const key = String(row.fieldKey)\n const bucketKey = `${recordId}::${key}`\n const orgId = row.organizationId ? String(row.organizationId) : null\n const tenantId = row.tenantId ? String(row.tenantId) : null\n const resolvedOrgId = orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null)\n const resolvedTenantId = tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? fallbackTenant)\n const def = pickDefinition(key, resolvedOrgId, resolvedTenantId)\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const value = valueFromRow(row)\n const decrypted = encrypted\n ? await decryptCustomFieldValue(\n value,\n resolvedTenantId ?? tenantId ?? null,\n getEncryptionService(),\n encryptionCache,\n { kind: def?.kind ?? null },\n )\n : value\n const existing = buckets.get(bucketKey)\n if (existing) {\n if (existing.orgId == null && resolvedOrgId) existing.orgId = resolvedOrgId\n if (existing.tenantId == null && resolvedTenantId) existing.tenantId = resolvedTenantId\n if (existing.def == null && def) existing.def = def\n existing.encrypted = existing.encrypted || encrypted\n existing.values.push(decrypted)\n } else {\n buckets.set(bucketKey, { orgId: resolvedOrgId, tenantId: resolvedTenantId, values: [decrypted], def: def ?? null, encrypted })\n }\n }\n\n const result: Record<string, Record<string, unknown>> = {}\n for (const [compoundKey, bucket] of buckets.entries()) {\n const [recordId, fieldKey] = compoundKey.split('::')\n if (!result[recordId]) result[recordId] = {}\n const prefixed = `cf_${fieldKey}`\n const def = bucket.def ?? pickDefinition(fieldKey, bucket.orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null), bucket.tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? null))\n if (def && def.configJson && typeof def.configJson === 'object' && (def.configJson as any).multi) {\n const cleaned = bucket.values.filter((v) => v !== undefined && v !== null)\n result[recordId][prefixed] = cleaned\n } else if (bucket.values.length > 1) {\n const cleaned = bucket.values.filter((v) => v !== undefined)\n result[recordId][prefixed] = cleaned\n } else {\n result[recordId][prefixed] = bucket.values[0] ?? null\n }\n }\n\n return result\n}\n\nexport function summarizeCustomFields(record: Record<string, unknown>): CustomFieldSnapshot {\n const entries = extractAllCustomFieldEntries(record)\n const values = Object.fromEntries(\n Object.entries(entries).map(([prefixedKey, value]) => [\n prefixedKey.replace(/^cf_/, ''),\n value,\n ]),\n )\n const customValues = Object.keys(values).length ? values : null\n const customFields = Object.entries(values).map(([key, value]) => ({\n key,\n label: key,\n value,\n kind: null,\n multi: Array.isArray(value),\n }))\n return { entries, customValues, customFields }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CustomFieldSet, EntityId } from '@open-mercato/shared/modules/entities'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { CustomFieldDef, CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'\nimport type { WhereValue } from '@open-mercato/shared/lib/query/types'\nimport type { TenantDataEncryptionService } from '../encryption/tenantDataEncryptionService'\nimport { decryptCustomFieldValue, resolveTenantEncryptionService } from '../encryption/customFieldValues'\nimport { parseBooleanToken } from '../boolean'\nimport { extractCustomFieldEntries } from './custom-fields-client'\n\nexport type CustomFieldSelectors = {\n keys: string[]\n selectors: string[] // e.g. ['cf:priority', 'cf:severity']\n outputKeys: string[] // e.g. ['cf_priority', 'cf_severity']\n}\n\nexport type SplitCustomFieldPayload = {\n base: Record<string, unknown>\n custom: Record<string, unknown>\n}\n\nexport type CustomFieldDefinitionSummary = {\n key: string\n label: string | null\n kind: string | null\n multi: boolean\n dictionaryId?: string | null\n organizationId?: string | null\n tenantId?: string | null\n priority: number\n updatedAt: number\n}\n\nexport type CustomFieldDefinitionIndex = Map<string, CustomFieldDefinitionSummary[]>\n\nexport type CustomFieldDisplayEntry = {\n key: string\n label: string | null\n value: unknown\n kind: string | null\n multi: boolean\n}\n\nexport type CustomFieldDisplayPayload = {\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport type CustomFieldSnapshot = {\n entries: Record<string, unknown>\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport function buildCustomFieldSelectorsForEntity(entityId: EntityId, sets: CustomFieldSet[]): CustomFieldSelectors {\n const keys = Array.from(new Set(\n (sets || [])\n .filter((s) => s.entity === entityId)\n .flatMap((s) => (s.fields || []).map((f) => f.key))\n ))\n const selectors = keys.map((k) => `cf:${k}`)\n const outputKeys = keys.map((k) => `cf_${k}`)\n return { keys, selectors, outputKeys }\n}\n\nexport function normalizeCustomFieldValue(val: unknown): unknown {\n if (Array.isArray(val)) return val\n if (typeof val === 'string') {\n const s = val.trim()\n // Parse Postgres array-like '{a,b,c}' to string[] when present\n if (s.startsWith('{') && s.endsWith('}')) {\n const inner = s.slice(1, -1).trim()\n if (!inner) return []\n return inner.split(/[\\s,]+/).map((x) => x.trim()).filter(Boolean)\n }\n return s\n }\n return val as any\n}\n\n// Extracts cf_* fields from a record that may contain both 'cf:<key>' and/or 'cf_<key>'\nexport function extractCustomFieldsFromItem(item: Record<string, unknown>, keys: string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of keys) {\n const colon = item[`cf:${key}` as keyof typeof item]\n const snake = item[`cf_${key}` as keyof typeof item]\n const value = colon !== undefined ? colon : snake\n if (value !== undefined) out[`cf_${key}`] = normalizeCustomFieldValue(value)\n }\n return out\n}\n\nexport function extractAllCustomFieldEntries(item: Record<string, unknown>): Record<string, unknown> {\n return extractCustomFieldEntries(item)\n}\n\nfunction normalizeFieldsetFilter(input?: string | string[] | null): Set<string | null> | null {\n if (input == null) return null\n const values = Array.isArray(input) ? input : [input]\n const normalized = new Set<string | null>()\n for (const raw of values) {\n if (raw == null) continue\n const trimmed = String(raw).trim()\n if (!trimmed) {\n normalized.add(null)\n } else {\n normalized.add(trimmed)\n }\n }\n return normalized.size ? normalized : null\n}\n\nexport async function buildCustomFieldFiltersFromQuery(opts: {\n entityId?: EntityId\n entityIds?: EntityId[]\n query: Record<string, unknown>\n em: EntityManager\n tenantId: string | null | undefined\n fieldset?: string | string[] | null\n}): Promise<Record<string, WhereValue>> {\n const out: Record<string, WhereValue> = {}\n const entries = Object.entries(opts.query).filter(([k]) => k.startsWith('cf_'))\n if (!entries.length) return out\n\n const entityIdList = Array.isArray(opts.entityIds) && opts.entityIds.length\n ? opts.entityIds\n : opts.entityId\n ? [opts.entityId]\n : []\n if (!entityIdList.length) return out\n\n // Tenant-only scope: allow global (null) or tenant match; ignore organization here\n const defs = await opts.em.find(CustomFieldDef, {\n entityId: { $in: entityIdList as any },\n isActive: true,\n $and: [\n { $or: [ { tenantId: opts.tenantId as any }, { tenantId: null } ] },\n ],\n })\n const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)\n const order = new Map<string, number>()\n entityIdList.map(String).forEach((id, index) => order.set(id, index))\n const byKey: Record<string, { kind: string; multi?: boolean; entityId: string }> = {}\n for (const d of defs) {\n if (fieldsetFilter) {\n const fieldsets = Array.isArray(d.configJson?.fieldsets)\n ? d.configJson.fieldsets\n .filter((entry: unknown): entry is string => typeof entry === 'string')\n .map((entry: string) => entry.trim())\n .filter((entry: string) => entry.length > 0)\n : []\n const rawFieldset = typeof d.configJson?.fieldset === 'string' ? d.configJson.fieldset.trim() : ''\n const normalizedFieldset = rawFieldset.length ? rawFieldset : null\n const matches = fieldsets.length > 0\n ? fieldsets.some((entry: string) => fieldsetFilter.has(entry))\n : fieldsetFilter.has(normalizedFieldset)\n if (!matches) continue\n }\n const key = d.key\n const entityId = String(d.entityId)\n const current = byKey[key]\n const rankNew = order.get(entityId) ?? Number.MAX_SAFE_INTEGER\n if (!current) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n continue\n }\n const rankOld = order.get(current.entityId) ?? Number.MAX_SAFE_INTEGER\n if (rankNew < rankOld) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n }\n }\n\n const coerce = (kind: string, v: unknown) => {\n if (v == null) return v as undefined\n switch (kind) {\n case 'integer': return Number.parseInt(String(v), 10)\n case 'float': return Number.parseFloat(String(v))\n case 'boolean': return parseBooleanToken(String(v)) === true\n case 'date':\n case 'datetime': return String(v)\n default: return String(v)\n }\n }\n\n for (const [rawKey, rawVal] of entries) {\n const isIn = rawKey.endsWith('In')\n const key = isIn ? rawKey.replace(/^cf_/, '').replace(/In$/, '') : rawKey.replace(/^cf_/, '')\n const def = byKey[key]\n const fieldId = `cf:${key}`\n if (!def) continue\n if (isIn) {\n const list = Array.isArray(rawVal)\n ? (rawVal as unknown[])\n : String(rawVal)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (list.length) out[fieldId] = { $in: list.map((x) => coerce(def.kind, x)) as (string[] | number[] | boolean[]) }\n } else {\n out[fieldId] = coerce(def.kind, rawVal)\n }\n }\n\n return out\n}\n\nexport function splitCustomFieldPayload(raw: unknown): SplitCustomFieldPayload {\n const base: Record<string, unknown> = {}\n const custom: Record<string, unknown> = {}\n if (!raw || typeof raw !== 'object') return { base, custom }\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (key === 'customFields') {\n if (Array.isArray(value)) {\n value.forEach((entry) => {\n if (!entry || typeof entry !== 'object') return\n const entryKey = typeof (entry as any).key === 'string' ? (entry as any).key.trim() : ''\n if (!entryKey) return\n custom[entryKey] = (entry as any).value\n })\n continue\n }\n if (value && typeof value === 'object') {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n const normalizedKey = typeof ck === 'string' ? ck.trim() : ''\n if (!normalizedKey) continue\n custom[normalizedKey] = cv\n }\n continue\n }\n }\n if (key === 'customValues' && value && typeof value === 'object' && !Array.isArray(value)) {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n custom[String(ck)] = cv\n }\n continue\n }\n if (key.startsWith('cf_')) {\n custom[key.slice(3)] = value\n continue\n }\n if (key.startsWith('cf:')) {\n custom[key.slice(3)] = value\n continue\n }\n base[key] = value\n }\n return { base, custom }\n}\n\nexport function extractCustomFieldValuesFromPayload(raw: Record<string, unknown>): Record<string, unknown> {\n return splitCustomFieldPayload(raw).custom\n}\n\nfunction normalizeDefinitionKey(key: unknown): string {\n if (typeof key !== 'string') return ''\n const trimmed = key.trim()\n return trimmed.length ? trimmed.toLowerCase() : ''\n}\n\nfunction normalizeDefinitionConfig(raw: unknown): Record<string, any> {\n if (!raw) return {}\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return { ...(parsed as Record<string, any>) }\n }\n return {}\n } catch {\n return {}\n }\n }\n if (typeof raw === 'object' && !Array.isArray(raw)) {\n return { ...(raw as Record<string, any>) }\n }\n return {}\n}\n\nfunction summarizeDefinition(def: CustomFieldDef): CustomFieldDefinitionSummary | null {\n const normalizedKey = normalizeDefinitionKey(def.key)\n if (!normalizedKey) return null\n const cfg = normalizeDefinitionConfig((def as any).configJson)\n const label =\n typeof cfg.label === 'string' && cfg.label.trim().length\n ? cfg.label.trim()\n : def.key\n const dictionaryId =\n typeof cfg.dictionaryId === 'string' && cfg.dictionaryId.trim().length\n ? cfg.dictionaryId.trim()\n : null\n const multi =\n cfg.multi !== undefined ? Boolean(cfg.multi) : false\n const priority =\n typeof cfg.priority === 'number' ? cfg.priority : 0\n const updatedAt =\n def.updatedAt instanceof Date\n ? def.updatedAt.getTime()\n : new Date(def.updatedAt as any).getTime()\n return {\n key: def.key,\n label,\n kind: typeof def.kind === 'string' ? def.kind : null,\n multi,\n dictionaryId,\n organizationId: def.organizationId ?? null,\n tenantId: def.tenantId ?? null,\n priority,\n updatedAt: Number.isNaN(updatedAt) ? 0 : updatedAt,\n }\n}\n\nfunction sortDefinitionSummaries(defs: CustomFieldDefinitionSummary[]): CustomFieldDefinitionSummary[] {\n return [...defs].sort((a, b) => {\n const priorityDiff = (a.priority ?? 0) - (b.priority ?? 0)\n if (priorityDiff !== 0) return priorityDiff\n const updatedDiff = (b.updatedAt ?? 0) - (a.updatedAt ?? 0)\n if (updatedDiff !== 0) return updatedDiff\n return a.key.localeCompare(b.key)\n })\n}\n\nfunction selectDefinitionForRecord(\n defs: CustomFieldDefinitionSummary[],\n organizationId: string | null,\n tenantId: string | null,\n): CustomFieldDefinitionSummary | null {\n if (!defs.length) return null\n const prioritizedForOrg = defs.filter(\n (def) => def.organizationId && organizationId && def.organizationId === organizationId,\n )\n if (prioritizedForOrg.length) return sortDefinitionSummaries(prioritizedForOrg)[0]\n const prioritizedForTenant = defs.filter(\n (def) => def.tenantId && tenantId && def.tenantId === tenantId && !def.organizationId,\n )\n if (prioritizedForTenant.length) return sortDefinitionSummaries(prioritizedForTenant)[0]\n const global = defs.filter((def) => !def.organizationId)\n if (global.length) return sortDefinitionSummaries(global)[0]\n return sortDefinitionSummaries(defs)[0] ?? null\n}\n\ntype LoadCustomFieldDefinitionIndexOptions = {\n em: EntityManager\n entityIds: string | string[]\n tenantId?: string | null | undefined\n organizationIds?: Array<string | null | undefined> | null\n fieldset?: string | string[] | null\n}\n\ntype CustomFieldDefIndexCache = {\n get(key: string): Promise<unknown> | unknown\n set(key: string, value: unknown, opts?: { ttl?: number; tags?: string[] }): Promise<unknown> | unknown\n deleteByTags?(tags: string[]): Promise<number> | number\n}\n\nconst CF_DEF_INDEX_CACHE_KEY_PREFIX = 'crud:cf-def-index'\n// Phase 2 default-off: integration runs observed `/api/customers/people`\n// returning 500 with this cache path active, and the readiness-probe\n// timeout blocked artifact upload so the direct stack trace was lost.\n// Until cross-request safety of the SQLite-cache JSON round-trip is\n// re-verified, ship with the layer disabled. Set\n// `OM_CF_DEF_CACHE_TTL_MS=300000` (or any positive integer) to opt in.\nconst CF_DEF_INDEX_DEFAULT_TTL_MS = 0\n\nfunction resolveCfDefIndexCacheTtlMs(): number {\n const raw = process.env.OM_CF_DEF_CACHE_TTL_MS\n if (raw === undefined) return CF_DEF_INDEX_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return CF_DEF_INDEX_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildCfDefIndexCacheKey(opts: {\n tenantId: string | null\n entityIds: string[]\n organizationIds: string[]\n fieldsetKey: string | null\n}): string {\n const tenant = opts.tenantId ?? 'global'\n const entities = opts.entityIds.slice().sort().join('|')\n const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort().join('|') : 'none'\n const fieldset = opts.fieldsetKey ?? 'all'\n return `${CF_DEF_INDEX_CACHE_KEY_PREFIX}:${tenant}:${entities}:${orgs}:${fieldset}`\n}\n\nfunction buildCfDefIndexCacheTags(opts: {\n tenantId: string | null\n entityIds: string[]\n}): string[] {\n const tenant = opts.tenantId ?? 'global'\n const tagBase = `entities:definitions:${tenant}`\n const tags = new Set<string>([tagBase])\n for (const entityId of opts.entityIds) {\n tags.add(`${tagBase}:entity:${entityId}`)\n }\n return Array.from(tags)\n}\n\nfunction normalizeFieldsetKey(value: string | string[] | null | undefined): string | null {\n if (!value) return null\n if (Array.isArray(value)) {\n const cleaned = value\n .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))\n .filter((entry) => entry.length > 0)\n if (!cleaned.length) return null\n return cleaned.sort().join(',')\n }\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction serializableIndexFromMap(index: CustomFieldDefinitionIndex): Array<[string, CustomFieldDefinitionSummary[]]> {\n return Array.from(index.entries())\n}\n\nfunction indexMapFromSerializable(value: unknown): CustomFieldDefinitionIndex | null {\n if (!Array.isArray(value)) return null\n const map: CustomFieldDefinitionIndex = new Map()\n for (const entry of value) {\n if (!Array.isArray(entry) || entry.length !== 2) return null\n const [key, summaries] = entry as [unknown, unknown]\n if (typeof key !== 'string' || !Array.isArray(summaries)) return null\n map.set(key, summaries as CustomFieldDefinitionSummary[])\n }\n return map\n}\n\n// Per-request micro-cache. Two CRUD calls within one HTTP request (rare but\n// possible via interceptors) share the same Map keyed by ctx-like objects.\nconst requestScopedCfDefIndexCache = new WeakMap<object, Map<string, CustomFieldDefinitionIndex>>()\n\nexport type CustomFieldDefinitionIndexCacheKey = string\n\nexport function getRequestScopedCfDefIndexCache(scope: object): Map<string, CustomFieldDefinitionIndex> {\n let bucket = requestScopedCfDefIndexCache.get(scope)\n if (!bucket) {\n bucket = new Map()\n requestScopedCfDefIndexCache.set(scope, bucket)\n }\n return bucket\n}\n\nasync function loadCustomFieldDefinitionIndexFresh(\n opts: LoadCustomFieldDefinitionIndexOptions & { entityIds: string[]; orgCandidates: string[] }\n): Promise<CustomFieldDefinitionIndex> {\n const { em, entityIds, orgCandidates } = opts\n const tenantId = opts.tenantId ?? null\n const scopeClauses: Record<string, unknown>[] = [\n tenantId\n ? { $or: [{ tenantId: tenantId as any }, { tenantId: null }] }\n : { tenantId: null },\n ]\n if (orgCandidates.length) {\n scopeClauses.push({\n $or: [{ organizationId: { $in: orgCandidates as any } }, { organizationId: null }],\n })\n } else {\n scopeClauses.push({ organizationId: null })\n }\n const where: Record<string, unknown> = {\n entityId: { $in: entityIds as any },\n deletedAt: null,\n isActive: true,\n $and: scopeClauses,\n }\n const defs = await em.find(CustomFieldDef, where as any)\n const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)\n const index: CustomFieldDefinitionIndex = new Map()\n defs.forEach((def) => {\n if (fieldsetFilter) {\n const config = normalizeDefinitionConfig((def as any).configJson)\n const fieldsets = Array.isArray(config.fieldsets)\n ? config.fieldsets\n .filter((entry: unknown): entry is string => typeof entry === 'string')\n .map((entry: string) => entry.trim())\n .filter((entry: string) => entry.length > 0)\n : []\n const fieldset = typeof config.fieldset === 'string' && config.fieldset.trim().length > 0\n ? config.fieldset.trim()\n : null\n const matches = fieldsets.length > 0\n ? fieldsets.some((entry: string) => fieldsetFilter.has(entry))\n : fieldsetFilter.has(fieldset)\n if (!matches) return\n }\n const summary = summarizeDefinition(def)\n if (!summary) return\n const normalizedKey = normalizeDefinitionKey(summary.key)\n if (!normalizedKey) return\n if (!index.has(normalizedKey)) index.set(normalizedKey, [])\n index.get(normalizedKey)!.push(summary)\n })\n index.forEach((entries, key) => {\n index.set(key, sortDefinitionSummaries(entries))\n })\n return index\n}\n\nexport async function loadCustomFieldDefinitionIndex(opts: LoadCustomFieldDefinitionIndexOptions & {\n cache?: CustomFieldDefIndexCache | null\n requestScope?: object | null\n}): Promise<CustomFieldDefinitionIndex> {\n const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds]\n const entityIds = list\n .map((id) => (typeof id === 'string' ? id.trim() : String(id ?? '')))\n .filter((id) => id.length > 0)\n if (!entityIds.length) return new Map()\n const tenantId = opts.tenantId ?? null\n const orgCandidates = Array.isArray(opts.organizationIds)\n ? opts.organizationIds\n .map((id) => (typeof id === 'string' ? id.trim() : id))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n : []\n\n const fieldsetKey = normalizeFieldsetKey(opts.fieldset)\n const ttlMs = resolveCfDefIndexCacheTtlMs()\n const cacheKey = buildCfDefIndexCacheKey({\n tenantId,\n entityIds,\n organizationIds: orgCandidates,\n fieldsetKey,\n })\n\n const requestBucket = opts.requestScope\n ? getRequestScopedCfDefIndexCache(opts.requestScope)\n : null\n if (requestBucket) {\n const cached = requestBucket.get(cacheKey)\n if (cached) return cached\n }\n\n const sharedCache = ttlMs > 0 ? opts.cache ?? null : null\n if (sharedCache && typeof sharedCache.get === 'function') {\n try {\n const cached = await sharedCache.get(cacheKey)\n const restored = indexMapFromSerializable(cached)\n if (restored) {\n if (requestBucket) requestBucket.set(cacheKey, restored)\n return restored\n }\n } catch (err) {\n console.warn('[crud:cf-def-cache] read failed', err)\n }\n }\n\n const index = await loadCustomFieldDefinitionIndexFresh({\n ...opts,\n entityIds,\n orgCandidates,\n })\n\n if (sharedCache && typeof sharedCache.set === 'function') {\n try {\n await sharedCache.set(cacheKey, serializableIndexFromMap(index), {\n ttl: ttlMs,\n tags: buildCfDefIndexCacheTags({ tenantId, entityIds }),\n })\n } catch (err) {\n console.warn('[crud:cf-def-cache] write failed', err)\n }\n }\n if (requestBucket) requestBucket.set(cacheKey, index)\n return index\n}\n\nexport type ApplyCustomFieldsNormalizationOptions = {\n /**\n * When true, removes raw `cf_*` and `cf:*` keys from the record once they\n * have been extracted into `customValues` / `customFields`. Produces a single\n * canonical response shape (issue #1769). Defaults to `false` to preserve the\n * existing wire format for callers that read `cf_*` from the top level.\n */\n stripPrefixedKeys?: boolean\n}\n\nexport function applyCustomFieldsNormalization(\n record: Record<string, unknown>,\n decorated: CustomFieldDisplayPayload,\n options: ApplyCustomFieldsNormalizationOptions = {},\n): Record<string, unknown> {\n const stripPrefixedKeys = options.stripPrefixedKeys === true\n const base: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (stripPrefixedKeys && (key.startsWith('cf_') || key.startsWith('cf:'))) continue\n base[key] = value\n }\n base.customValues = decorated.customValues\n base.customFields = decorated.customFields\n return base\n}\n\nexport function decorateRecordWithCustomFields(\n record: Record<string, unknown>,\n definitions: CustomFieldDefinitionIndex,\n context: {\n organizationId?: string | null\n tenantId?: string | null\n } = {},\n): CustomFieldDisplayPayload {\n const rawEntries = extractAllCustomFieldEntries(record)\n if (!Object.keys(rawEntries).length) {\n return { customValues: null, customFields: [] }\n }\n const values: Record<string, unknown> = {}\n const entries: Array<{ entry: CustomFieldDisplayEntry; priority: number; updatedAt: number }> = []\n const organizationId = context.organizationId ?? null\n const tenantId = context.tenantId ?? null\n\n Object.entries(rawEntries).forEach(([prefixedKey, value]) => {\n const bareKey = prefixedKey.replace(/^cf_/, '')\n const normalizedKey = normalizeDefinitionKey(bareKey)\n if (!normalizedKey) return\n const defsForKey = definitions.get(normalizedKey) ?? []\n const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId)\n // Skip custom field values without active definitions to prevent orphaned fields\n if (!resolvedDef) return\n values[bareKey] = value\n const entry: CustomFieldDisplayEntry = {\n key: bareKey,\n label: resolvedDef.label ?? bareKey,\n value,\n kind: resolvedDef.kind ?? null,\n multi: resolvedDef.multi ?? Array.isArray(value),\n }\n entries.push({\n entry,\n priority: resolvedDef.priority ?? Number.MAX_SAFE_INTEGER,\n updatedAt: resolvedDef.updatedAt ?? 0,\n })\n })\n\n const ordered = entries\n .sort((a, b) => {\n const priorityDiff = a.priority - b.priority\n if (priorityDiff !== 0) return priorityDiff\n const updatedDiff = b.updatedAt - a.updatedAt\n if (updatedDiff !== 0) return updatedDiff\n return a.entry.key.localeCompare(b.entry.key)\n })\n .map((item) => item.entry)\n\n return {\n customValues: Object.keys(values).length ? values : null,\n customFields: ordered,\n }\n}\n\nexport async function loadCustomFieldValues(opts: {\n em: EntityManager\n entityId: EntityId\n recordIds: string[]\n tenantIdByRecord?: Record<string, string | null | undefined>\n organizationIdByRecord?: Record<string, string | null | undefined>\n tenantFallbacks?: (string | null | undefined)[]\n encryptionService?: TenantDataEncryptionService | null\n}): Promise<Record<string, Record<string, unknown>>> {\n const { em, entityId, recordIds } = opts\n if (!Array.isArray(recordIds) || recordIds.length === 0) return {}\n\n const normalizedRecordIds = recordIds.map((id) => String(id))\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em, opts.encryptionService)\n return encryptionService\n }\n const tenantCandidates = new Set<string | null>()\n tenantCandidates.add(null)\n if (opts.tenantIdByRecord) {\n for (const val of Object.values(opts.tenantIdByRecord)) {\n tenantCandidates.add(val ? String(val) : null)\n }\n }\n if (opts.tenantFallbacks) {\n for (const val of opts.tenantFallbacks) tenantCandidates.add(val ? String(val) : null)\n }\n const fallbackTenant = (opts.tenantFallbacks || []).find((t) => t != null) ?? null\n\n const tenantList = Array.from(tenantCandidates)\n const tenantNonNull = tenantList.filter((t): t is string => t !== null)\n const tenantFilter = tenantNonNull.length\n ? { tenantId: { $in: [...tenantNonNull, null] as any } }\n : { tenantId: null }\n const cfRows = await em.find(CustomFieldValue, {\n entityId: entityId as any,\n recordId: { $in: normalizedRecordIds as any },\n deletedAt: null,\n ...(tenantList.length ? tenantFilter : {}),\n })\n\n if (!cfRows.length) return {}\n\n const allKeys = Array.from(new Set(cfRows.map((row) => String(row.fieldKey))))\n const organizationCandidates = new Set<string | null>()\n organizationCandidates.add(null)\n if (opts.organizationIdByRecord) {\n for (const val of Object.values(opts.organizationIdByRecord)) {\n organizationCandidates.add(val ? String(val) : null)\n }\n }\n for (const row of cfRows) {\n organizationCandidates.add(row.organizationId ? String(row.organizationId) : null)\n }\n const orgList = Array.from(organizationCandidates)\n\n const defs = allKeys.length\n ? await em.find(CustomFieldDef, {\n entityId: entityId as any,\n key: { $in: allKeys as any },\n deletedAt: null,\n isActive: true,\n ...(tenantList.length ? { tenantId: tenantFilter.tenantId } : {}),\n organizationId: { $in: orgList as any },\n })\n : []\n\n const defsByKey = new Map<string, CustomFieldDef[]>()\n for (const def of defs) {\n const list = defsByKey.get(def.key) || []\n list.push(def)\n defsByKey.set(def.key, list)\n }\n\n const pickDefinition = (fieldKey: string, organizationId: string | null, tenantId: string | null) => {\n const candidates = defsByKey.get(fieldKey)\n if (!candidates || candidates.length === 0) return null\n const active = candidates.filter((opt) => opt.isActive !== false && !opt.deletedAt)\n const list = active.length ? active : candidates\n if (organizationId && tenantId) {\n const exact = list.find((opt) => opt.organizationId === organizationId && opt.tenantId === tenantId)\n if (exact) return exact\n }\n if (organizationId) {\n const orgMatch = list.find((opt) => opt.organizationId === organizationId && (!tenantId || opt.tenantId == null || opt.tenantId === tenantId))\n if (orgMatch) return orgMatch\n }\n if (tenantId) {\n const tenantMatch = list.find((opt) => opt.organizationId == null && opt.tenantId === tenantId)\n if (tenantMatch) return tenantMatch\n }\n const global = list.find((opt) => opt.organizationId == null && opt.tenantId == null)\n return global ?? list[0]\n }\n\n const valueFromRow = (row: CustomFieldValue): unknown => {\n if (row.valueMultiline !== null && row.valueMultiline !== undefined) return row.valueMultiline\n if (row.valueText !== null && row.valueText !== undefined) return row.valueText\n if (row.valueInt !== null && row.valueInt !== undefined) return row.valueInt\n if (row.valueFloat !== null && row.valueFloat !== undefined) return row.valueFloat\n if (row.valueBool !== null && row.valueBool !== undefined) return row.valueBool\n return null\n }\n\n type Bucket = { orgId: string | null; tenantId: string | null; values: unknown[]; def?: CustomFieldDef | null; encrypted?: boolean }\n const buckets = new Map<string, Bucket>()\n\n for (const row of cfRows) {\n const recordId = String(row.recordId)\n const key = String(row.fieldKey)\n const bucketKey = `${recordId}::${key}`\n const orgId = row.organizationId ? String(row.organizationId) : null\n const tenantId = row.tenantId ? String(row.tenantId) : null\n const resolvedOrgId = orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null)\n const resolvedTenantId = tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? fallbackTenant)\n const def = pickDefinition(key, resolvedOrgId, resolvedTenantId)\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const value = valueFromRow(row)\n const decrypted = encrypted\n ? await decryptCustomFieldValue(\n value,\n resolvedTenantId ?? tenantId ?? null,\n getEncryptionService(),\n encryptionCache,\n { kind: def?.kind ?? null },\n )\n : value\n const existing = buckets.get(bucketKey)\n if (existing) {\n if (existing.orgId == null && resolvedOrgId) existing.orgId = resolvedOrgId\n if (existing.tenantId == null && resolvedTenantId) existing.tenantId = resolvedTenantId\n if (existing.def == null && def) existing.def = def\n existing.encrypted = existing.encrypted || encrypted\n existing.values.push(decrypted)\n } else {\n buckets.set(bucketKey, { orgId: resolvedOrgId, tenantId: resolvedTenantId, values: [decrypted], def: def ?? null, encrypted })\n }\n }\n\n const result: Record<string, Record<string, unknown>> = {}\n for (const [compoundKey, bucket] of buckets.entries()) {\n const [recordId, fieldKey] = compoundKey.split('::')\n if (!result[recordId]) result[recordId] = {}\n const prefixed = `cf_${fieldKey}`\n const def = bucket.def ?? pickDefinition(fieldKey, bucket.orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null), bucket.tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? null))\n if (def && def.configJson && typeof def.configJson === 'object' && (def.configJson as any).multi) {\n const cleaned = bucket.values.filter((v) => v !== undefined && v !== null)\n result[recordId][prefixed] = cleaned\n } else if (bucket.values.length > 1) {\n const cleaned = bucket.values.filter((v) => v !== undefined)\n result[recordId][prefixed] = cleaned\n } else {\n result[recordId][prefixed] = bucket.values[0] ?? null\n }\n }\n\n return result\n}\n\nexport function summarizeCustomFields(record: Record<string, unknown>): CustomFieldSnapshot {\n const entries = extractAllCustomFieldEntries(record)\n const values = Object.fromEntries(\n Object.entries(entries).map(([prefixedKey, value]) => [\n prefixedKey.replace(/^cf_/, ''),\n value,\n ]),\n )\n const customValues = Object.keys(values).length ? values : null\n const customFields = Object.entries(values).map(([key, value]) => ({\n key,\n label: key,\n value,\n kind: null,\n multi: Array.isArray(value),\n }))\n return { entries, customValues, customFields }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -371,6 +371,23 @@ function resolveAccessLogService(container) {
371
371
  }
372
372
  return null;
373
373
  }
374
+ function shouldBlockAccessLogWrites() {
375
+ return process.env.OM_CRUD_ACCESS_LOG_BLOCKING === "1";
376
+ }
377
+ const pendingCrudAccessLogPromises = /* @__PURE__ */ new Set();
378
+ function trackPendingCrudAccessLogPromise(promise) {
379
+ pendingCrudAccessLogPromises.add(promise);
380
+ promise.catch(() => void 0).finally(() => {
381
+ pendingCrudAccessLogPromises.delete(promise);
382
+ });
383
+ return promise;
384
+ }
385
+ async function flushPendingCrudAccessLogs() {
386
+ while (pendingCrudAccessLogPromises.size > 0) {
387
+ const snapshot = Array.from(pendingCrudAccessLogPromises);
388
+ await Promise.allSettled(snapshot);
389
+ }
390
+ }
374
391
  function logForbidden(details) {
375
392
  try {
376
393
  console.warn("[crud] Forbidden request", details);
@@ -406,10 +423,10 @@ function createCrudProfiler(resource, operation) {
406
423
  }
407
424
  async function logCrudAccess(options) {
408
425
  const { container, auth, request, items, resourceKind } = options;
409
- if (!auth) return;
410
- if (!Array.isArray(items) || items.length === 0) return;
426
+ if (!auth) return { mode: "skipped", count: 0, pending: pendingCrudAccessLogPromises.size };
427
+ if (!Array.isArray(items) || items.length === 0) return { mode: "skipped", count: 0, pending: pendingCrudAccessLogPromises.size };
411
428
  const service = resolveAccessLogService(container);
412
- if (!service) return;
429
+ if (!service) return { mode: "skipped", count: 0, pending: pendingCrudAccessLogPromises.size };
413
430
  const idField = options.idField || "id";
414
431
  const tenantId = options.tenantId ?? auth.tenantId ?? null;
415
432
  const organizationId = options.organizationId ?? auth.orgId ?? null;
@@ -431,7 +448,7 @@ async function logCrudAccess(options) {
431
448
  } catch {
432
449
  }
433
450
  const uniqueIds = /* @__PURE__ */ new Set();
434
- const tasks = [];
451
+ const payloads = [];
435
452
  for (const item of items) {
436
453
  if (!item || typeof item !== "object") continue;
437
454
  const rawId = item[idField];
@@ -448,17 +465,41 @@ async function logCrudAccess(options) {
448
465
  };
449
466
  if (fields.length > 0) payload.fields = fields;
450
467
  if (Object.keys(context).length > 0) payload.context = context;
451
- tasks.push(
452
- Promise.resolve(service.log(payload)).catch((err) => {
453
- try {
454
- console.error("[crud] failed to record access log", { err, payload });
455
- } catch {
456
- }
457
- return void 0;
458
- })
459
- );
468
+ payloads.push(payload);
469
+ }
470
+ if (!payloads.length) return { mode: "skipped", count: 0, pending: pendingCrudAccessLogPromises.size };
471
+ const blocking = shouldBlockAccessLogWrites();
472
+ const dispatchMode = typeof service.logMany === "function" ? "batch" : "fanout";
473
+ const writePromise = (async () => {
474
+ try {
475
+ if (typeof service.logMany === "function") {
476
+ await service.logMany(payloads);
477
+ } else {
478
+ await Promise.all(
479
+ payloads.map(
480
+ (payload) => Promise.resolve(service.log(payload)).catch((err) => {
481
+ try {
482
+ console.error("[crud] failed to record access log", { err, payload });
483
+ } catch {
484
+ }
485
+ return void 0;
486
+ })
487
+ )
488
+ );
489
+ }
490
+ } catch (err) {
491
+ try {
492
+ console.error("[crud] failed to record access logs (batch)", { err, count: payloads.length });
493
+ } catch {
494
+ }
495
+ }
496
+ })();
497
+ trackPendingCrudAccessLogPromise(writePromise);
498
+ if (blocking) {
499
+ await writePromise;
500
+ return { mode: "blocking", count: payloads.length, pending: pendingCrudAccessLogPromises.size };
460
501
  }
461
- if (tasks.length > 0) await Promise.all(tasks);
502
+ return { mode: dispatchMode, count: payloads.length, pending: pendingCrudAccessLogPromises.size };
462
503
  }
463
504
  function safeClone(value) {
464
505
  try {
@@ -582,11 +623,18 @@ function makeCrudRoute(opts) {
582
623
  try {
583
624
  const em = ctx.container.resolve("em");
584
625
  const organizationIds = Array.isArray(ctx.organizationIds) && ctx.organizationIds.length ? ctx.organizationIds : [ctx.selectedOrganizationId ?? null];
626
+ let cfDefCache = null;
627
+ try {
628
+ cfDefCache = ctx.container.resolve("cache");
629
+ } catch {
630
+ }
585
631
  const definitionIndex = await loadCustomFieldDefinitionIndex({
586
632
  em,
587
633
  entityIds,
588
634
  tenantId: ctx.auth?.tenantId ?? null,
589
- organizationIds
635
+ organizationIds,
636
+ cache: cfDefCache ?? null,
637
+ requestScope: ctx
590
638
  });
591
639
  cfProfiler.mark("definitions_loaded", { definitionCount: definitionIndex.size });
592
640
  const decoratedItems = items.map((raw) => {
@@ -619,20 +667,31 @@ function makeCrudRoute(opts) {
619
667
  return items;
620
668
  }
621
669
  };
670
+ const userFeaturesPromiseCache = /* @__PURE__ */ new WeakMap();
671
+ function resolveUserFeaturesOnce(ctx) {
672
+ if (!ctx.auth) return Promise.resolve(void 0);
673
+ const cached = userFeaturesPromiseCache.get(ctx);
674
+ if (cached) return cached;
675
+ const promise = (async () => {
676
+ try {
677
+ const rbac = ctx.container.resolve("rbacService");
678
+ if (rbac?.getGrantedFeatures) {
679
+ return await rbac.getGrantedFeatures(ctx.auth.sub, {
680
+ tenantId: ctx.auth.tenantId,
681
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId
682
+ });
683
+ }
684
+ } catch {
685
+ }
686
+ return void 0;
687
+ })();
688
+ userFeaturesPromiseCache.set(ctx, promise);
689
+ return promise;
690
+ }
622
691
  async function buildEnricherContext(ctx) {
623
692
  if (!opts.enrichers?.entityId) return null;
624
693
  if (!ctx.auth) return null;
625
- let userFeatures;
626
- try {
627
- const rbac = ctx.container.resolve("rbacService");
628
- if (rbac?.getGrantedFeatures) {
629
- userFeatures = await rbac.getGrantedFeatures(ctx.auth.sub, {
630
- tenantId: ctx.auth.tenantId,
631
- organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId
632
- });
633
- }
634
- } catch {
635
- }
694
+ const userFeatures = await resolveUserFeaturesOnce(ctx);
636
695
  return {
637
696
  organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? "",
638
697
  tenantId: ctx.auth.tenantId ?? "",
@@ -643,18 +702,7 @@ function makeCrudRoute(opts) {
643
702
  };
644
703
  }
645
704
  async function resolveUserFeatures(ctx) {
646
- if (!ctx.auth) return void 0;
647
- try {
648
- const rbac = ctx.container.resolve("rbacService");
649
- if (rbac?.getGrantedFeatures) {
650
- return await rbac.getGrantedFeatures(ctx.auth.sub, {
651
- tenantId: ctx.auth.tenantId,
652
- organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId
653
- });
654
- }
655
- } catch {
656
- }
657
- return void 0;
705
+ return resolveUserFeaturesOnce(ctx);
658
706
  }
659
707
  const interceptorContextCache = /* @__PURE__ */ new WeakMap();
660
708
  async function buildInterceptorContextInner(ctx) {
@@ -1106,7 +1154,7 @@ function makeCrudRoute(opts) {
1106
1154
  }
1107
1155
  profiler.mark("translation_overlays_complete", { itemCount: transformedItems.length });
1108
1156
  }
1109
- await logCrudAccess({
1157
+ const accessLogResult2 = await logCrudAccess({
1110
1158
  container: ctx.container,
1111
1159
  auth: ctx.auth,
1112
1160
  request,
@@ -1117,7 +1165,7 @@ function makeCrudRoute(opts) {
1117
1165
  tenantId: ctx.auth.tenantId ?? null,
1118
1166
  query: validated
1119
1167
  });
1120
- profiler.mark("access_logged");
1168
+ profiler.mark("access_logged", accessLogResult2);
1121
1169
  if (exportRequested && requestedExport) {
1122
1170
  const total = typeof res.total === "number" ? res.total : rawItems.length;
1123
1171
  const initialExportItems = exportFullRequested ? rawItems.map(normalizeFullRecordForExport) : transformedItems;
@@ -1315,7 +1363,7 @@ function makeCrudRoute(opts) {
1315
1363
  }
1316
1364
  profiler.mark("fallback_translation_overlays_complete", { itemCount: Array.isArray(list) ? list.length : 0 });
1317
1365
  }
1318
- await logCrudAccess({
1366
+ const accessLogResult = await logCrudAccess({
1319
1367
  container: ctx.container,
1320
1368
  auth: ctx.auth,
1321
1369
  request,
@@ -1326,7 +1374,7 @@ function makeCrudRoute(opts) {
1326
1374
  tenantId: ctx.auth.tenantId ?? null,
1327
1375
  query: validated
1328
1376
  });
1329
- profiler.mark("access_logged");
1377
+ profiler.mark("access_logged", accessLogResult);
1330
1378
  if (exportRequested && requestedExport) {
1331
1379
  const exportItems = exportFullRequested ? list.map(normalizeFullRecordForExport) : list;
1332
1380
  const prepared = exportFullRequested ? { columns: ensureColumns(exportItems), rows: exportItems } : prepareExportData(exportItems, opts.list);
@@ -2190,6 +2238,7 @@ function makeCrudRoute(opts) {
2190
2238
  return { metadata, GET, POST, PUT, DELETE };
2191
2239
  }
2192
2240
  export {
2241
+ flushPendingCrudAccessLogs,
2193
2242
  logCrudAccess,
2194
2243
  makeCrudRoute
2195
2244
  };