@open-mercato/shared 0.6.6-develop.6339.1.193c6c7c71 → 0.6.6-develop.6344.1.c4b17c07c4

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.
@@ -1,2 +1,2 @@
1
- [build:shared] found 225 entry points
1
+ [build:shared] found 226 entry points
2
2
  [build:shared] built successfully
@@ -46,7 +46,7 @@ function walkFiles(dirPath) {
46
46
  return files;
47
47
  }
48
48
  function listGeneratedCacheFiles(appRoot) {
49
- return walkFiles(getGeneratedDir(appRoot)).filter((filePath) => filePath.endsWith(".mjs")).sort();
49
+ return walkFiles(getGeneratedDir(appRoot)).filter((filePath) => filePath.endsWith(".mjs")).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
50
50
  }
51
51
  function findStaleGeneratedCacheFiles(appRoot) {
52
52
  const generatedFiles = listGeneratedCacheFiles(appRoot);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/bootstrap/generatedCacheRecovery.ts"],
4
- "sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\n\nconst DECORATOR_EXPORT_NAMES = [\n 'Entity',\n 'PrimaryKey',\n 'Property',\n 'ManyToOne',\n 'OneToMany',\n 'OneToOne',\n 'ManyToMany',\n 'Enum',\n 'Index',\n 'Unique',\n 'Embeddable',\n 'Embedded',\n 'Formula',\n] as const\n\nconst RECOVERY_VERSION = 'mikro-orm-v7-generated-cache-recovery-v1'\nconst RECOVERY_MARKER_FILE = '.mikro-orm-v7-cache-recovery.json'\nconst GENERATED_DIR_SEGMENTS = ['.mercato', 'generated'] as const\n\nconst staleDecoratorImportPattern = new RegExp(\n String.raw`import\\s*\\{[^}]*\\b(?:${DECORATOR_EXPORT_NAMES.join('|')})\\b[^}]*\\}\\s*from\\s*['\"]@mikro-orm/core['\"]`,\n 'm',\n)\n\nconst decoratorExportErrorPattern = /@mikro-orm\\/core' does not provide an export named '(?:Entity|PrimaryKey|Property|ManyToOne|OneToMany|OneToOne|ManyToMany|Enum|Index|Unique|Embeddable|Embedded|Formula)'/\n\ntype RecoveryLogger = {\n warn: (message: string) => void\n}\n\ntype RecoveryReason = 'stale-generated-cache-scan' | 'runtime-import-error'\n\ntype RecoveryMarker = {\n version: string\n reason: RecoveryReason\n createdAt: string\n deletedFiles: string[]\n}\n\nexport type GeneratedCacheRecoveryResult = {\n applied: boolean\n deletedFiles: string[]\n markerPath: string | null\n}\n\nfunction getGeneratedDir(appRoot: string): string {\n return path.join(appRoot, ...GENERATED_DIR_SEGMENTS)\n}\n\nfunction getRecoveryMarkerPath(appRoot: string): string {\n return path.join(getGeneratedDir(appRoot), RECOVERY_MARKER_FILE)\n}\n\nfunction walkFiles(dirPath: string): string[] {\n if (!fs.existsSync(dirPath)) return []\n\n const entries = fs.readdirSync(dirPath, { withFileTypes: true })\n const files: string[] = []\n for (const entry of entries) {\n const absolutePath = path.join(dirPath, entry.name)\n if (entry.isDirectory()) {\n files.push(...walkFiles(absolutePath))\n continue\n }\n if (entry.isFile()) {\n files.push(absolutePath)\n }\n }\n return files\n}\n\nfunction listGeneratedCacheFiles(appRoot: string): string[] {\n return walkFiles(getGeneratedDir(appRoot))\n .filter((filePath) => filePath.endsWith('.mjs'))\n .sort()\n}\n\nfunction findStaleGeneratedCacheFiles(appRoot: string): string[] {\n const generatedFiles = listGeneratedCacheFiles(appRoot)\n return generatedFiles.filter((filePath) => {\n const source = fs.readFileSync(filePath, 'utf8')\n return staleDecoratorImportPattern.test(source)\n })\n}\n\nfunction writeRecoveryMarker(appRoot: string, marker: RecoveryMarker): string {\n const markerPath = getRecoveryMarkerPath(appRoot)\n fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2))\n return markerPath\n}\n\nfunction logRecoveryMessage(logger: RecoveryLogger, reason: RecoveryReason): void {\n const header =\n reason === 'runtime-import-error'\n ? '\u26A0\uFE0F Open Mercato detected a stale generated cache while bootstrapping the app.'\n : '\u26A0\uFE0F Open Mercato detected stale generated cache from the MikroORM v7 migration.'\n\n logger.warn('')\n logger.warn(header)\n logger.warn('\uD83D\uDCD8 Open Mercato migrated MikroORM to version 7. Please review UPGRADE_NOTES.md and the `migrate-mikro-orm` skill if your code still imports decorators from `@mikro-orm/core`.')\n logger.warn('\uD83E\uDDF9 Cleaning generated compilation cache and recompiling generated code now...')\n logger.warn('')\n}\n\nfunction deleteGeneratedCacheFiles(filePaths: string[]): string[] {\n const deletedFiles: string[] = []\n for (const filePath of filePaths) {\n if (!fs.existsSync(filePath)) continue\n fs.rmSync(filePath, { force: true })\n deletedFiles.push(filePath)\n }\n return deletedFiles\n}\n\nfunction applyGeneratedCacheRecovery(\n appRoot: string,\n staleFiles: string[],\n reason: RecoveryReason,\n logger: RecoveryLogger,\n): GeneratedCacheRecoveryResult {\n if (staleFiles.length === 0) {\n return { applied: false, deletedFiles: [], markerPath: null }\n }\n\n logRecoveryMessage(logger, reason)\n\n const generatedCacheFiles = listGeneratedCacheFiles(appRoot)\n const deletedFiles = deleteGeneratedCacheFiles(generatedCacheFiles)\n const markerPath = writeRecoveryMarker(appRoot, {\n version: RECOVERY_VERSION,\n reason,\n createdAt: new Date().toISOString(),\n deletedFiles,\n })\n\n return {\n applied: true,\n deletedFiles,\n markerPath,\n }\n}\n\nexport function shouldRecoverMikroOrmV7GeneratedCache(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return decoratorExportErrorPattern.test(message)\n}\n\nexport function ensureMikroOrmV7GeneratedCacheCompatibility(\n appRoot: string,\n options: { logger?: RecoveryLogger } = {},\n): GeneratedCacheRecoveryResult {\n const logger = options.logger ?? console\n const staleFiles = findStaleGeneratedCacheFiles(appRoot)\n return applyGeneratedCacheRecovery(appRoot, staleFiles, 'stale-generated-cache-scan', logger)\n}\n\nexport function recoverMikroOrmV7GeneratedCacheFromImportError(\n appRoot: string,\n error: unknown,\n options: { logger?: RecoveryLogger } = {},\n): GeneratedCacheRecoveryResult {\n if (!shouldRecoverMikroOrmV7GeneratedCache(error)) {\n return { applied: false, deletedFiles: [], markerPath: null }\n }\n\n const logger = options.logger ?? console\n const staleFiles = findStaleGeneratedCacheFiles(appRoot)\n return applyGeneratedCacheRecovery(appRoot, staleFiles, 'runtime-import-error', logger)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\n\nconst DECORATOR_EXPORT_NAMES = [\n 'Entity',\n 'PrimaryKey',\n 'Property',\n 'ManyToOne',\n 'OneToMany',\n 'OneToOne',\n 'ManyToMany',\n 'Enum',\n 'Index',\n 'Unique',\n 'Embeddable',\n 'Embedded',\n 'Formula',\n] as const\n\nconst RECOVERY_VERSION = 'mikro-orm-v7-generated-cache-recovery-v1'\nconst RECOVERY_MARKER_FILE = '.mikro-orm-v7-cache-recovery.json'\nconst GENERATED_DIR_SEGMENTS = ['.mercato', 'generated'] as const\n\nconst staleDecoratorImportPattern = new RegExp(\n String.raw`import\\s*\\{[^}]*\\b(?:${DECORATOR_EXPORT_NAMES.join('|')})\\b[^}]*\\}\\s*from\\s*['\"]@mikro-orm/core['\"]`,\n 'm',\n)\n\nconst decoratorExportErrorPattern = /@mikro-orm\\/core' does not provide an export named '(?:Entity|PrimaryKey|Property|ManyToOne|OneToMany|OneToOne|ManyToMany|Enum|Index|Unique|Embeddable|Embedded|Formula)'/\n\ntype RecoveryLogger = {\n warn: (message: string) => void\n}\n\ntype RecoveryReason = 'stale-generated-cache-scan' | 'runtime-import-error'\n\ntype RecoveryMarker = {\n version: string\n reason: RecoveryReason\n createdAt: string\n deletedFiles: string[]\n}\n\nexport type GeneratedCacheRecoveryResult = {\n applied: boolean\n deletedFiles: string[]\n markerPath: string | null\n}\n\nfunction getGeneratedDir(appRoot: string): string {\n return path.join(appRoot, ...GENERATED_DIR_SEGMENTS)\n}\n\nfunction getRecoveryMarkerPath(appRoot: string): string {\n return path.join(getGeneratedDir(appRoot), RECOVERY_MARKER_FILE)\n}\n\nfunction walkFiles(dirPath: string): string[] {\n if (!fs.existsSync(dirPath)) return []\n\n const entries = fs.readdirSync(dirPath, { withFileTypes: true })\n const files: string[] = []\n for (const entry of entries) {\n const absolutePath = path.join(dirPath, entry.name)\n if (entry.isDirectory()) {\n files.push(...walkFiles(absolutePath))\n continue\n }\n if (entry.isFile()) {\n files.push(absolutePath)\n }\n }\n return files\n}\n\nfunction listGeneratedCacheFiles(appRoot: string): string[] {\n return walkFiles(getGeneratedDir(appRoot))\n .filter((filePath) => filePath.endsWith('.mjs'))\n .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n}\n\nfunction findStaleGeneratedCacheFiles(appRoot: string): string[] {\n const generatedFiles = listGeneratedCacheFiles(appRoot)\n return generatedFiles.filter((filePath) => {\n const source = fs.readFileSync(filePath, 'utf8')\n return staleDecoratorImportPattern.test(source)\n })\n}\n\nfunction writeRecoveryMarker(appRoot: string, marker: RecoveryMarker): string {\n const markerPath = getRecoveryMarkerPath(appRoot)\n fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2))\n return markerPath\n}\n\nfunction logRecoveryMessage(logger: RecoveryLogger, reason: RecoveryReason): void {\n const header =\n reason === 'runtime-import-error'\n ? '\u26A0\uFE0F Open Mercato detected a stale generated cache while bootstrapping the app.'\n : '\u26A0\uFE0F Open Mercato detected stale generated cache from the MikroORM v7 migration.'\n\n logger.warn('')\n logger.warn(header)\n logger.warn('\uD83D\uDCD8 Open Mercato migrated MikroORM to version 7. Please review UPGRADE_NOTES.md and the `migrate-mikro-orm` skill if your code still imports decorators from `@mikro-orm/core`.')\n logger.warn('\uD83E\uDDF9 Cleaning generated compilation cache and recompiling generated code now...')\n logger.warn('')\n}\n\nfunction deleteGeneratedCacheFiles(filePaths: string[]): string[] {\n const deletedFiles: string[] = []\n for (const filePath of filePaths) {\n if (!fs.existsSync(filePath)) continue\n fs.rmSync(filePath, { force: true })\n deletedFiles.push(filePath)\n }\n return deletedFiles\n}\n\nfunction applyGeneratedCacheRecovery(\n appRoot: string,\n staleFiles: string[],\n reason: RecoveryReason,\n logger: RecoveryLogger,\n): GeneratedCacheRecoveryResult {\n if (staleFiles.length === 0) {\n return { applied: false, deletedFiles: [], markerPath: null }\n }\n\n logRecoveryMessage(logger, reason)\n\n const generatedCacheFiles = listGeneratedCacheFiles(appRoot)\n const deletedFiles = deleteGeneratedCacheFiles(generatedCacheFiles)\n const markerPath = writeRecoveryMarker(appRoot, {\n version: RECOVERY_VERSION,\n reason,\n createdAt: new Date().toISOString(),\n deletedFiles,\n })\n\n return {\n applied: true,\n deletedFiles,\n markerPath,\n }\n}\n\nexport function shouldRecoverMikroOrmV7GeneratedCache(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return decoratorExportErrorPattern.test(message)\n}\n\nexport function ensureMikroOrmV7GeneratedCacheCompatibility(\n appRoot: string,\n options: { logger?: RecoveryLogger } = {},\n): GeneratedCacheRecoveryResult {\n const logger = options.logger ?? console\n const staleFiles = findStaleGeneratedCacheFiles(appRoot)\n return applyGeneratedCacheRecovery(appRoot, staleFiles, 'stale-generated-cache-scan', logger)\n}\n\nexport function recoverMikroOrmV7GeneratedCacheFromImportError(\n appRoot: string,\n error: unknown,\n options: { logger?: RecoveryLogger } = {},\n): GeneratedCacheRecoveryResult {\n if (!shouldRecoverMikroOrmV7GeneratedCache(error)) {\n return { applied: false, deletedFiles: [], markerPath: null }\n }\n\n const logger = options.logger ?? console\n const staleFiles = findStaleGeneratedCacheFiles(appRoot)\n return applyGeneratedCacheRecovery(appRoot, staleFiles, 'runtime-import-error', logger)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -167,8 +167,8 @@ function resolveCfDefIndexCacheTtlMs() {
167
167
  }
168
168
  function buildCfDefIndexCacheKey(opts) {
169
169
  const tenant = opts.tenantId ?? "global";
170
- const entities = opts.entityIds.slice().sort().join("|");
171
- const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort().join("|") : "none";
170
+ const entities = opts.entityIds.slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join("|");
171
+ const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join("|") : "none";
172
172
  const fieldset = opts.fieldsetKey ?? "all";
173
173
  return `${CF_DEF_INDEX_CACHE_KEY_PREFIX}:${tenant}:${entities}:${orgs}:${fieldset}`;
174
174
  }
@@ -186,7 +186,7 @@ function normalizeFieldsetKey(value) {
186
186
  if (Array.isArray(value)) {
187
187
  const cleaned = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter((entry) => entry.length > 0);
188
188
  if (!cleaned.length) return null;
189
- return cleaned.sort().join(",");
189
+ return cleaned.sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join(",");
190
190
  }
191
191
  if (typeof value !== "string") return null;
192
192
  const trimmed = value.trim();
@@ -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'\nimport {\n buildCustomFieldDefinitionIndexFromRows,\n normalizeDefinitionKey,\n normalizeFieldsetFilter,\n selectDefinitionForRecord,\n type CustomFieldDefinitionIndex,\n type CustomFieldDefinitionRow,\n type CustomFieldDefinitionSummary,\n} from './custom-field-definition-index'\n\nexport type { CustomFieldDefinitionSummary, CustomFieldDefinitionIndex } from './custom-field-definition-index'\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 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\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\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 rows: CustomFieldDefinitionRow[] = defs.map((def) => ({\n key: def.key,\n entityId: String((def as any).entityId),\n kind: typeof def.kind === 'string' ? def.kind : null,\n configJson: (def as any).configJson,\n organizationId: def.organizationId ?? null,\n tenantId: def.tenantId ?? null,\n deletedAt: (def as any).deletedAt ?? null,\n updatedAt: (def as any).updatedAt ?? null,\n }))\n return buildCustomFieldDefinitionIndexFromRows(rows, {\n organizationIds: orgCandidates,\n fieldset: opts.fieldset,\n })\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 const rowInfos = cfRows.map((row) => {\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 return { bucketKey, resolvedOrgId, resolvedTenantId, tenantId, def, encrypted, value }\n })\n\n // Decrypt every encrypted value concurrently so a list of N rows \u00D7 M encrypted\n // fields costs the slowest single decryption rather than the sum (issue #2229).\n // The shared encryptionCache keeps DEK lookups deduped per tenant.\n const decryptedValues = await Promise.all(\n rowInfos.map((info) =>\n info.encrypted\n ? decryptCustomFieldValue(\n info.value,\n info.resolvedTenantId ?? info.tenantId ?? null,\n getEncryptionService(),\n encryptionCache,\n { kind: info.def?.kind ?? null },\n )\n : info.value,\n ),\n )\n\n rowInfos.forEach((info, index) => {\n const decrypted = decryptedValues[index]\n const existing = buckets.get(info.bucketKey)\n if (existing) {\n if (existing.orgId == null && info.resolvedOrgId) existing.orgId = info.resolvedOrgId\n if (existing.tenantId == null && info.resolvedTenantId) existing.tenantId = info.resolvedTenantId\n if (existing.def == null && info.def) existing.def = info.def\n existing.encrypted = existing.encrypted || info.encrypted\n existing.values.push(decrypted)\n } else {\n buckets.set(info.bucketKey, { orgId: info.resolvedOrgId, tenantId: info.resolvedTenantId, values: [decrypted], def: info.def ?? null, encrypted: info.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'\nimport {\n buildCustomFieldDefinitionIndexFromRows,\n normalizeDefinitionKey,\n normalizeFieldsetFilter,\n selectDefinitionForRecord,\n type CustomFieldDefinitionIndex,\n type CustomFieldDefinitionRow,\n type CustomFieldDefinitionSummary,\n} from './custom-field-definition-index'\n\nexport type { CustomFieldDefinitionSummary, CustomFieldDefinitionIndex } from './custom-field-definition-index'\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 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\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\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((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join('|')\n const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).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((a, b) => (a < b ? -1 : a > b ? 1 : 0)).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 rows: CustomFieldDefinitionRow[] = defs.map((def) => ({\n key: def.key,\n entityId: String((def as any).entityId),\n kind: typeof def.kind === 'string' ? def.kind : null,\n configJson: (def as any).configJson,\n organizationId: def.organizationId ?? null,\n tenantId: def.tenantId ?? null,\n deletedAt: (def as any).deletedAt ?? null,\n updatedAt: (def as any).updatedAt ?? null,\n }))\n return buildCustomFieldDefinitionIndexFromRows(rows, {\n organizationIds: orgCandidates,\n fieldset: opts.fieldset,\n })\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 const rowInfos = cfRows.map((row) => {\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 return { bucketKey, resolvedOrgId, resolvedTenantId, tenantId, def, encrypted, value }\n })\n\n // Decrypt every encrypted value concurrently so a list of N rows \u00D7 M encrypted\n // fields costs the slowest single decryption rather than the sum (issue #2229).\n // The shared encryptionCache keeps DEK lookups deduped per tenant.\n const decryptedValues = await Promise.all(\n rowInfos.map((info) =>\n info.encrypted\n ? decryptCustomFieldValue(\n info.value,\n info.resolvedTenantId ?? info.tenantId ?? null,\n getEncryptionService(),\n encryptionCache,\n { kind: info.def?.kind ?? null },\n )\n : info.value,\n ),\n )\n\n rowInfos.forEach((info, index) => {\n const decrypted = decryptedValues[index]\n const existing = buckets.get(info.bucketKey)\n if (existing) {\n if (existing.orgId == null && info.resolvedOrgId) existing.orgId = info.resolvedOrgId\n if (existing.tenantId == null && info.resolvedTenantId) existing.tenantId = info.resolvedTenantId\n if (existing.def == null && info.def) existing.def = info.def\n existing.encrypted = existing.encrypted || info.encrypted\n existing.values.push(decrypted)\n } else {\n buckets.set(info.bucketKey, { orgId: info.resolvedOrgId, tenantId: info.resolvedTenantId, values: [decrypted], def: info.def ?? null, encrypted: info.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
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/mutation-guard.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\n\nexport type CrudMutationGuardValidationSuccess = {\n ok: true\n shouldRunAfterSuccess: boolean\n metadata?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardValidationFailure = {\n ok: false\n status: number\n body: Record<string, unknown>\n}\n\nexport type CrudMutationGuardValidationResult =\n | CrudMutationGuardValidationSuccess\n | CrudMutationGuardValidationFailure\n\nexport type CrudMutationGuardValidateInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardAfterSuccessInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n}\n\ntype CrudMutationGuardServiceLike = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\nfunction resolveCrudMutationGuardService(container: AwilixContainer): CrudMutationGuardServiceLike | null {\n try {\n const service = container.resolve<CrudMutationGuardServiceLike>('crudMutationGuardService')\n if (!service) return null\n if (typeof service.validateMutation !== 'function') return null\n if (typeof service.afterMutationSuccess !== 'function') return null\n return service\n } catch {\n return null\n }\n}\n\n/**\n * @deprecated Use `runMutationGuards()` from `@open-mercato/shared/lib/crud/mutation-guard-registry` instead.\n * This function is bridged internally via `bridgeLegacyGuard()` and will be removed in a future release.\n */\nexport async function validateCrudMutationGuard(\n container: AwilixContainer,\n input: CrudMutationGuardValidateInput,\n): Promise<CrudMutationGuardValidationResult | null> {\n const service = resolveCrudMutationGuardService(container)\n if (!service) return null\n return service.validateMutation(input)\n}\n\n/**\n * @deprecated Use `runMutationGuards()` afterSuccess callbacks from `@open-mercato/shared/lib/crud/mutation-guard-registry` instead.\n * This function is bridged internally via `bridgeLegacyGuard()` and will be removed in a future release.\n */\nexport async function runCrudMutationGuardAfterSuccess(\n container: AwilixContainer,\n input: CrudMutationGuardAfterSuccessInput,\n): Promise<void> {\n const service = resolveCrudMutationGuardService(container)\n if (!service) return\n try {\n await service.afterMutationSuccess(input)\n } catch (error) {\n console.error('[crud-mutation-guard] after-success hook failed', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n requestMethod: input.requestMethod,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n}\n"],
5
- "mappings": "AA+CA,SAAS,gCAAgC,WAAiE;AACxG,MAAI;AACF,UAAM,UAAU,UAAU,QAAsC,0BAA0B;AAC1F,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,OAAO,QAAQ,qBAAqB,WAAY,QAAO;AAC3D,QAAI,OAAO,QAAQ,yBAAyB,WAAY,QAAO;AAC/D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,0BACpB,WACA,OACmD;AACnD,QAAM,UAAU,gCAAgC,SAAS;AACzD,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,iBAAiB,KAAK;AACvC;AAMA,eAAsB,iCACpB,WACA,OACe;AACf,QAAM,UAAU,gCAAgC,SAAS;AACzD,MAAI,CAAC,QAAS;AACd,MAAI;AACF,UAAM,QAAQ,qBAAqB,KAAK;AAAA,EAC1C,SAAS,OAAO;AACd,YAAQ,MAAM,mDAAmD;AAAA,MAC/D,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,eAAe,MAAM;AAAA,MACrB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAC9D,CAAC;AAAA,EACH;AACF;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\n\nexport type CrudMutationGuardValidationSuccess = {\n ok: true\n shouldRunAfterSuccess: boolean\n metadata?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardValidationFailure = {\n ok: false\n status: number\n body: Record<string, unknown>\n}\n\nexport type CrudMutationGuardValidationResult =\n | CrudMutationGuardValidationSuccess\n | CrudMutationGuardValidationFailure\n\nexport type CrudMutationGuardValidateInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardAfterSuccessInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n}\n\ntype CrudMutationGuardServiceLike = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\nfunction resolveCrudMutationGuardService(container: AwilixContainer): CrudMutationGuardServiceLike | null {\n try {\n const service = container.resolve<CrudMutationGuardServiceLike>('crudMutationGuardService')\n if (!service) return null\n if (typeof service.validateMutation !== 'function') return null\n if (typeof service.afterMutationSuccess !== 'function') return null\n return service\n } catch {\n return null\n }\n}\n\n/**\n * @deprecated Resolves ONLY the single DI-registered `crudMutationGuardService`,\n * so it silently bypasses every guard in the global mutation-guard store\n * (`getAllMutationGuardInstances()`). Use the full registry instead:\n * `runRouteMutationGuards()` from `@open-mercato/shared/lib/crud/route-mutation-guard`\n * for custom write routes, or `runMutationGuards()` from\n * `@open-mercato/shared/lib/crud/mutation-guard-registry` directly. The legacy\n * service is still honored on the modern path via `bridgeLegacyGuard()`. This\n * function will be removed in a future release.\n */\nexport async function validateCrudMutationGuard(\n container: AwilixContainer,\n input: CrudMutationGuardValidateInput,\n): Promise<CrudMutationGuardValidationResult | null> {\n const service = resolveCrudMutationGuardService(container)\n if (!service) return null\n return service.validateMutation(input)\n}\n\n/**\n * @deprecated Runs ONLY the single DI-registered `crudMutationGuardService`'s\n * after-success hook, skipping the registry guards' `afterSuccess` callbacks.\n * Use the `runAfterSuccess()` returned by `runRouteMutationGuards()` from\n * `@open-mercato/shared/lib/crud/route-mutation-guard`, or the\n * `afterSuccessCallbacks` returned by `runMutationGuards()` from\n * `@open-mercato/shared/lib/crud/mutation-guard-registry`. This function will be\n * removed in a future release.\n */\nexport async function runCrudMutationGuardAfterSuccess(\n container: AwilixContainer,\n input: CrudMutationGuardAfterSuccessInput,\n): Promise<void> {\n const service = resolveCrudMutationGuardService(container)\n if (!service) return\n try {\n await service.afterMutationSuccess(input)\n } catch (error) {\n console.error('[crud-mutation-guard] after-success hook failed', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n requestMethod: input.requestMethod,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n}\n"],
5
+ "mappings": "AA+CA,SAAS,gCAAgC,WAAiE;AACxG,MAAI;AACF,UAAM,UAAU,UAAU,QAAsC,0BAA0B;AAC1F,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,OAAO,QAAQ,qBAAqB,WAAY,QAAO;AAC3D,QAAI,OAAO,QAAQ,yBAAyB,WAAY,QAAO;AAC/D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYA,eAAsB,0BACpB,WACA,OACmD;AACnD,QAAM,UAAU,gCAAgC,SAAS;AACzD,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,iBAAiB,KAAK;AACvC;AAWA,eAAsB,iCACpB,WACA,OACe;AACf,QAAM,UAAU,gCAAgC,SAAS;AACzD,MAAI,CAAC,QAAS;AACd,MAAI;AACF,UAAM,QAAQ,qBAAqB,KAAK;AAAA,EAC1C,SAAS,OAAO;AACd,YAAQ,MAAM,mDAAmD;AAAA,MAC/D,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,eAAe,MAAM;AAAA,MACrB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAC9D,CAAC;AAAA,EACH;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,84 @@
1
+ import {
2
+ bridgeLegacyGuard,
3
+ runMutationGuards
4
+ } from "./mutation-guard-registry.js";
5
+ import { getAllMutationGuardInstances } from "./mutation-guard-store.js";
6
+ function toRegistryMutationOperation(operation) {
7
+ if (operation === "create" || operation === "delete") return operation;
8
+ return "update";
9
+ }
10
+ async function resolveRouteUserFeatures(container, auth) {
11
+ if (auth.userFeatures) return auth.userFeatures;
12
+ try {
13
+ const rbac = container.resolve("rbacService");
14
+ if (rbac?.getGrantedFeatures) {
15
+ return await rbac.getGrantedFeatures(auth.userId, {
16
+ tenantId: auth.tenantId,
17
+ organizationId: auth.organizationId ?? null
18
+ });
19
+ }
20
+ } catch {
21
+ }
22
+ return [];
23
+ }
24
+ async function runRouteMutationGuards(params) {
25
+ const { container, req, auth, input } = params;
26
+ const operation = toRegistryMutationOperation(input.operation);
27
+ const allGuards = [...getAllMutationGuardInstances()];
28
+ const legacyGuard = bridgeLegacyGuard(container);
29
+ if (legacyGuard) allGuards.push(legacyGuard);
30
+ const userFeatures = await resolveRouteUserFeatures(container, auth);
31
+ const guardResult = await runMutationGuards(
32
+ allGuards,
33
+ {
34
+ tenantId: auth.tenantId,
35
+ organizationId: auth.organizationId ?? null,
36
+ userId: auth.userId,
37
+ resourceKind: input.resourceKind,
38
+ resourceId: input.resourceId ?? null,
39
+ operation,
40
+ requestMethod: req.method,
41
+ requestHeaders: req.headers,
42
+ mutationPayload: input.mutationPayload ?? null
43
+ },
44
+ { userFeatures }
45
+ );
46
+ if (!guardResult.ok) {
47
+ const errorStatus = guardResult.errorStatus ?? 422;
48
+ const errorBody = guardResult.errorBody ?? { error: "Operation blocked by guard" };
49
+ return {
50
+ ok: false,
51
+ errorStatus,
52
+ errorBody,
53
+ response: Response.json(errorBody, { status: errorStatus })
54
+ };
55
+ }
56
+ return {
57
+ ok: true,
58
+ modifiedPayload: guardResult.modifiedPayload,
59
+ runAfterSuccess: async () => {
60
+ for (const { guard, metadata } of guardResult.afterSuccessCallbacks) {
61
+ try {
62
+ await guard.afterSuccess({
63
+ tenantId: auth.tenantId,
64
+ organizationId: auth.organizationId ?? null,
65
+ userId: auth.userId,
66
+ resourceKind: input.resourceKind,
67
+ resourceId: input.resourceId ?? "",
68
+ operation,
69
+ requestMethod: req.method,
70
+ requestHeaders: req.headers,
71
+ metadata
72
+ });
73
+ } catch (error) {
74
+ console.error(`[mutation-guard] afterSuccess failed for guard ${guard.id}`, error);
75
+ }
76
+ }
77
+ }
78
+ };
79
+ }
80
+ export {
81
+ runRouteMutationGuards,
82
+ toRegistryMutationOperation
83
+ };
84
+ //# sourceMappingURL=route-mutation-guard.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/crud/route-mutation-guard.ts"],
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n} from './mutation-guard-registry'\nimport { getAllMutationGuardInstances } from './mutation-guard-store'\n\n/**\n * Shared registry-based mutation-guard wrapper for custom write routes that do\n * not use `makeCrudRoute`.\n *\n * This mirrors `collectAndRunGuards()` in `factory.ts`: it runs **every** guard\n * collected from the global mutation-guard store (`getAllMutationGuardInstances()`)\n * plus the bridged legacy DI service (`bridgeLegacyGuard()`), so a custom route\n * enforces the same guard set as every `makeCrudRoute` write.\n *\n * Prefer this over the deprecated `validateCrudMutationGuard()` /\n * `runCrudMutationGuardAfterSuccess()` pair, which resolve only the single\n * DI-registered `crudMutationGuardService` and silently skip registry guards.\n */\n\nexport type RouteMutationGuardOperation = 'create' | 'update' | 'delete' | 'custom'\n\nexport type RouteMutationGuardAuth = {\n userId: string\n tenantId: string\n organizationId?: string | null\n /**\n * Pre-resolved granted features for the caller. When omitted, the wrapper\n * resolves them via `rbacService.getGrantedFeatures` (same source the CRUD\n * factory uses). Feature-gated registry guards only run when their required\n * features are present, so supplying the wrong set silently skips guards.\n */\n userFeatures?: string[]\n}\n\nexport type RouteMutationGuardInput = {\n resourceKind: string\n resourceId?: string | null\n /**\n * The route's logical operation. `'custom'` (state-changing action endpoints)\n * is mapped to the closest registry operation, `'update'`, because\n * `runMutationGuards` only understands `create | update | delete`.\n */\n operation?: RouteMutationGuardOperation\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type RouteMutationGuardBlocked = {\n ok: false\n errorStatus: number\n errorBody: Record<string, unknown>\n /** Ready-to-return JSON response built from `errorBody` / `errorStatus`. */\n response: Response\n}\n\nexport type RouteMutationGuardPassed = {\n ok: true\n /** Merged payload when a guard transformed it; `undefined` when unchanged. */\n modifiedPayload?: Record<string, unknown>\n /**\n * Runs every guard's `afterSuccess` callback that requested it. Callback\n * failures are caught and logged so a committed write still succeeds \u2014 call\n * this only after the mutation has committed.\n */\n runAfterSuccess: () => Promise<void>\n}\n\nexport type RouteMutationGuardResult = RouteMutationGuardBlocked | RouteMutationGuardPassed\n\ntype RbacServiceLike = {\n getGrantedFeatures: (\n userId: string,\n opts: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n}\n\n/**\n * Map a route-level operation to the registry operation set. State-changing\n * action endpoints (`'custom'`) and `'update'` both map to `'update'` per the\n * `packages/core/AGENTS.md` \u2192 API Routes guidance.\n */\nexport function toRegistryMutationOperation(\n operation: RouteMutationGuardOperation | undefined,\n): 'create' | 'update' | 'delete' {\n if (operation === 'create' || operation === 'delete') return operation\n return 'update'\n}\n\nasync function resolveRouteUserFeatures(\n container: AwilixContainer,\n auth: RouteMutationGuardAuth,\n): Promise<string[]> {\n if (auth.userFeatures) return auth.userFeatures\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike | undefined\n if (rbac?.getGrantedFeatures) {\n return await rbac.getGrantedFeatures(auth.userId, {\n tenantId: auth.tenantId,\n organizationId: auth.organizationId ?? null,\n })\n }\n } catch {\n // rbacService not available \u2014 guards without feature requirements still run.\n }\n return []\n}\n\n/**\n * Collect every registered mutation guard plus the bridged legacy DI service,\n * run them against the route's mutation, and return either a blocked result\n * (with a ready response) or a passed result carrying the merged payload and an\n * after-success runner.\n */\nexport async function runRouteMutationGuards(params: {\n container: AwilixContainer\n req: Request\n auth: RouteMutationGuardAuth\n input: RouteMutationGuardInput\n}): Promise<RouteMutationGuardResult> {\n const { container, req, auth, input } = params\n const operation = toRegistryMutationOperation(input.operation)\n\n const allGuards: MutationGuard[] = [...getAllMutationGuardInstances()]\n const legacyGuard = bridgeLegacyGuard(container)\n if (legacyGuard) allGuards.push(legacyGuard)\n\n const userFeatures = await resolveRouteUserFeatures(container, auth)\n\n const guardResult = await runMutationGuards(\n allGuards,\n {\n tenantId: auth.tenantId,\n organizationId: auth.organizationId ?? null,\n userId: auth.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId ?? null,\n operation,\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input.mutationPayload ?? null,\n },\n { userFeatures },\n )\n\n if (!guardResult.ok) {\n const errorStatus = guardResult.errorStatus ?? 422\n const errorBody = guardResult.errorBody ?? { error: 'Operation blocked by guard' }\n return {\n ok: false,\n errorStatus,\n errorBody,\n response: Response.json(errorBody, { status: errorStatus }),\n }\n }\n\n return {\n ok: true,\n modifiedPayload: guardResult.modifiedPayload,\n runAfterSuccess: async () => {\n for (const { guard, metadata } of guardResult.afterSuccessCallbacks) {\n try {\n await guard.afterSuccess!({\n tenantId: auth.tenantId,\n organizationId: auth.organizationId ?? null,\n userId: auth.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId ?? '',\n operation,\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata,\n })\n } catch (error) {\n console.error(`[mutation-guard] afterSuccess failed for guard ${guard.id}`, error)\n }\n }\n },\n }\n}\n"],
5
+ "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oCAAoC;AA6EtC,SAAS,4BACd,WACgC;AAChC,MAAI,cAAc,YAAY,cAAc,SAAU,QAAO;AAC7D,SAAO;AACT;AAEA,eAAe,yBACb,WACA,MACmB;AACnB,MAAI,KAAK,aAAc,QAAO,KAAK;AACnC,MAAI;AACF,UAAM,OAAO,UAAU,QAAQ,aAAa;AAC5C,QAAI,MAAM,oBAAoB;AAC5B,aAAO,MAAM,KAAK,mBAAmB,KAAK,QAAQ;AAAA,QAChD,UAAU,KAAK;AAAA,QACf,gBAAgB,KAAK,kBAAkB;AAAA,MACzC,CAAC;AAAA,IACH;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,CAAC;AACV;AAQA,eAAsB,uBAAuB,QAKP;AACpC,QAAM,EAAE,WAAW,KAAK,MAAM,MAAM,IAAI;AACxC,QAAM,YAAY,4BAA4B,MAAM,SAAS;AAE7D,QAAM,YAA6B,CAAC,GAAG,6BAA6B,CAAC;AACrE,QAAM,cAAc,kBAAkB,SAAS;AAC/C,MAAI,YAAa,WAAU,KAAK,WAAW;AAE3C,QAAM,eAAe,MAAM,yBAAyB,WAAW,IAAI;AAEnE,QAAM,cAAc,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK,kBAAkB;AAAA,MACvC,QAAQ,KAAK;AAAA,MACb,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM,cAAc;AAAA,MAChC;AAAA,MACA,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,iBAAiB,MAAM,mBAAmB;AAAA,IAC5C;AAAA,IACA,EAAE,aAAa;AAAA,EACjB;AAEA,MAAI,CAAC,YAAY,IAAI;AACnB,UAAM,cAAc,YAAY,eAAe;AAC/C,UAAM,YAAY,YAAY,aAAa,EAAE,OAAO,6BAA6B;AACjF,WAAO;AAAA,MACL,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,MACA,UAAU,SAAS,KAAK,WAAW,EAAE,QAAQ,YAAY,CAAC;AAAA,IAC5D;AAAA,EACF;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,iBAAiB,YAAY;AAAA,IAC7B,iBAAiB,YAAY;AAC3B,iBAAW,EAAE,OAAO,SAAS,KAAK,YAAY,uBAAuB;AACnE,YAAI;AACF,gBAAM,MAAM,aAAc;AAAA,YACxB,UAAU,KAAK;AAAA,YACf,gBAAgB,KAAK,kBAAkB;AAAA,YACvC,QAAQ,KAAK;AAAA,YACb,cAAc,MAAM;AAAA,YACpB,YAAY,MAAM,cAAc;AAAA,YAChC;AAAA,YACA,eAAe,IAAI;AAAA,YACnB,gBAAgB,IAAI;AAAA,YACpB;AAAA,UACF,CAAC;AAAA,QACH,SAAS,OAAO;AACd,kBAAQ,MAAM,kDAAkD,MAAM,EAAE,IAAI,KAAK;AAAA,QACnF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.6-develop.6339.1.193c6c7c71";
1
+ const APP_VERSION = "0.6.6-develop.6344.1.c4b17c07c4";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6-develop.6339.1.193c6c7c71'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6-develop.6344.1.c4b17c07c4'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.6.6-develop.6339.1.193c6c7c71",
3
+ "version": "0.6.6-develop.6344.1.c4b17c07c4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -93,7 +93,7 @@
93
93
  "@mikro-orm/core": "^7.1.4",
94
94
  "@mikro-orm/decorators": "^7.1.4",
95
95
  "@mikro-orm/postgresql": "^7.1.4",
96
- "@open-mercato/cache": "0.6.6-develop.6339.1.193c6c7c71",
96
+ "@open-mercato/cache": "0.6.6-develop.6344.1.c4b17c07c4",
97
97
  "dotenv": "^17.4.2",
98
98
  "rate-limiter-flexible": "^11.2.0",
99
99
  "re2js": "2.8.3",
@@ -76,7 +76,7 @@ function walkFiles(dirPath: string): string[] {
76
76
  function listGeneratedCacheFiles(appRoot: string): string[] {
77
77
  return walkFiles(getGeneratedDir(appRoot))
78
78
  .filter((filePath) => filePath.endsWith('.mjs'))
79
- .sort()
79
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
80
80
  }
81
81
 
82
82
  function findStaleGeneratedCacheFiles(appRoot: string): string[] {
@@ -0,0 +1,260 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import { registerMutationGuards } from '../mutation-guard-store'
3
+ import type { MutationGuard } from '../mutation-guard-registry'
4
+ import { runRouteMutationGuards, toRegistryMutationOperation } from '../route-mutation-guard'
5
+
6
+ type Registrations = Record<string, unknown>
7
+
8
+ function makeContainer(registrations: Registrations = {}): AwilixContainer {
9
+ return {
10
+ resolve: (name: string) => {
11
+ if (Object.prototype.hasOwnProperty.call(registrations, name)) return registrations[name]
12
+ throw new Error(`[test] no registration for ${name}`)
13
+ },
14
+ } as unknown as AwilixContainer
15
+ }
16
+
17
+ function makeRequest(method = 'POST'): Request {
18
+ return new Request('https://example.test/api/things', { method })
19
+ }
20
+
21
+ function makeGuard(overrides: Partial<MutationGuard> & { id: string }): MutationGuard {
22
+ return {
23
+ targetEntity: 'things.thing',
24
+ operations: ['create', 'update', 'delete'],
25
+ priority: 50,
26
+ validate: jest.fn().mockResolvedValue({ ok: true }),
27
+ ...overrides,
28
+ }
29
+ }
30
+
31
+ function registerStoreGuards(guards: MutationGuard[]) {
32
+ registerMutationGuards([{ moduleId: 'things', guards }])
33
+ }
34
+
35
+ const baseAuth = {
36
+ userId: 'user-1',
37
+ tenantId: 'tenant-1',
38
+ organizationId: 'org-1',
39
+ userFeatures: [] as string[],
40
+ }
41
+ const baseInput = {
42
+ resourceKind: 'things.thing',
43
+ resourceId: 'thing-1',
44
+ operation: 'update' as const,
45
+ mutationPayload: { title: 'Updated' },
46
+ }
47
+
48
+ afterEach(() => {
49
+ registerMutationGuards([])
50
+ jest.restoreAllMocks()
51
+ })
52
+
53
+ describe('toRegistryMutationOperation', () => {
54
+ it('passes create and delete through and maps update/custom/undefined to update', () => {
55
+ expect(toRegistryMutationOperation('create')).toBe('create')
56
+ expect(toRegistryMutationOperation('delete')).toBe('delete')
57
+ expect(toRegistryMutationOperation('update')).toBe('update')
58
+ expect(toRegistryMutationOperation('custom')).toBe('update')
59
+ expect(toRegistryMutationOperation(undefined)).toBe('update')
60
+ })
61
+ })
62
+
63
+ describe('runRouteMutationGuards', () => {
64
+ it('runs a registry-store guard the legacy bridge path would have skipped', async () => {
65
+ const guard = makeGuard({ id: 'store-guard' })
66
+ registerStoreGuards([guard])
67
+
68
+ const result = await runRouteMutationGuards({
69
+ container: makeContainer(),
70
+ req: makeRequest(),
71
+ auth: baseAuth,
72
+ input: baseInput,
73
+ })
74
+
75
+ expect(result.ok).toBe(true)
76
+ expect(guard.validate).toHaveBeenCalledTimes(1)
77
+ })
78
+
79
+ it('blocks and returns a ready response when a store guard rejects', async () => {
80
+ const guard = makeGuard({
81
+ id: 'blocking-guard',
82
+ validate: jest.fn().mockResolvedValue({ ok: false, status: 422, body: { error: 'nope' } }),
83
+ })
84
+ registerStoreGuards([guard])
85
+
86
+ const result = await runRouteMutationGuards({
87
+ container: makeContainer(),
88
+ req: makeRequest(),
89
+ auth: baseAuth,
90
+ input: baseInput,
91
+ })
92
+
93
+ expect(result.ok).toBe(false)
94
+ if (result.ok) throw new Error('expected blocked result')
95
+ expect(result.errorStatus).toBe(422)
96
+ expect(result.errorBody).toEqual({ error: 'nope' })
97
+ expect(result.response.status).toBe(422)
98
+ await expect(result.response.json()).resolves.toEqual({ error: 'nope' })
99
+ })
100
+
101
+ it('threads modifiedPayload from a store guard', async () => {
102
+ const guard = makeGuard({
103
+ id: 'transform-guard',
104
+ validate: jest.fn().mockResolvedValue({ ok: true, modifiedPayload: { extra: true } }),
105
+ })
106
+ registerStoreGuards([guard])
107
+
108
+ const result = await runRouteMutationGuards({
109
+ container: makeContainer(),
110
+ req: makeRequest(),
111
+ auth: baseAuth,
112
+ input: baseInput,
113
+ })
114
+
115
+ expect(result.ok).toBe(true)
116
+ if (!result.ok) throw new Error('expected passed result')
117
+ expect(result.modifiedPayload).toEqual({ title: 'Updated', extra: true })
118
+ })
119
+
120
+ it('runs afterSuccess callbacks via runAfterSuccess()', async () => {
121
+ const afterSuccess = jest.fn().mockResolvedValue(undefined)
122
+ const guard = makeGuard({
123
+ id: 'after-guard',
124
+ validate: jest.fn().mockResolvedValue({ ok: true, shouldRunAfterSuccess: true, metadata: { ping: 1 } }),
125
+ afterSuccess,
126
+ })
127
+ registerStoreGuards([guard])
128
+
129
+ const result = await runRouteMutationGuards({
130
+ container: makeContainer(),
131
+ req: makeRequest(),
132
+ auth: baseAuth,
133
+ input: baseInput,
134
+ })
135
+
136
+ expect(result.ok).toBe(true)
137
+ if (!result.ok) throw new Error('expected passed result')
138
+ expect(afterSuccess).not.toHaveBeenCalled()
139
+
140
+ await result.runAfterSuccess()
141
+
142
+ expect(afterSuccess).toHaveBeenCalledTimes(1)
143
+ expect(afterSuccess).toHaveBeenCalledWith(
144
+ expect.objectContaining({ resourceKind: 'things.thing', resourceId: 'thing-1', metadata: { ping: 1 } }),
145
+ )
146
+ })
147
+
148
+ it('swallows afterSuccess callback errors so a committed write still succeeds', async () => {
149
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
150
+ const guard = makeGuard({
151
+ id: 'throwing-after-guard',
152
+ validate: jest.fn().mockResolvedValue({ ok: true, shouldRunAfterSuccess: true }),
153
+ afterSuccess: jest.fn().mockRejectedValue(new Error('after boom')),
154
+ })
155
+ registerStoreGuards([guard])
156
+
157
+ const result = await runRouteMutationGuards({
158
+ container: makeContainer(),
159
+ req: makeRequest(),
160
+ auth: baseAuth,
161
+ input: baseInput,
162
+ })
163
+
164
+ expect(result.ok).toBe(true)
165
+ if (!result.ok) throw new Error('expected passed result')
166
+ await expect(result.runAfterSuccess()).resolves.toBeUndefined()
167
+ expect(consoleError).toHaveBeenCalled()
168
+ })
169
+
170
+ it('skips feature-gated guards when the caller lacks the feature', async () => {
171
+ const guard = makeGuard({
172
+ id: 'gated-guard',
173
+ features: ['premium.locks'],
174
+ validate: jest.fn().mockResolvedValue({ ok: false, message: 'should not run' }),
175
+ })
176
+ registerStoreGuards([guard])
177
+
178
+ const passes = await runRouteMutationGuards({
179
+ container: makeContainer(),
180
+ req: makeRequest(),
181
+ auth: { ...baseAuth, userFeatures: [] },
182
+ input: baseInput,
183
+ })
184
+ expect(passes.ok).toBe(true)
185
+ expect(guard.validate).not.toHaveBeenCalled()
186
+
187
+ const blocks = await runRouteMutationGuards({
188
+ container: makeContainer(),
189
+ req: makeRequest(),
190
+ auth: { ...baseAuth, userFeatures: ['premium.locks'] },
191
+ input: baseInput,
192
+ })
193
+ expect(blocks.ok).toBe(false)
194
+ })
195
+
196
+ it('resolves userFeatures from rbacService when not pre-supplied', async () => {
197
+ const getGrantedFeatures = jest.fn().mockResolvedValue(['premium.locks'])
198
+ const guard = makeGuard({
199
+ id: 'gated-guard',
200
+ features: ['premium.locks'],
201
+ validate: jest.fn().mockResolvedValue({ ok: false, message: 'blocked via rbac-resolved features' }),
202
+ })
203
+ registerStoreGuards([guard])
204
+
205
+ const result = await runRouteMutationGuards({
206
+ container: makeContainer({ rbacService: { getGrantedFeatures } }),
207
+ req: makeRequest(),
208
+ auth: { userId: 'user-1', tenantId: 'tenant-1', organizationId: 'org-1' },
209
+ input: baseInput,
210
+ })
211
+
212
+ expect(getGrantedFeatures).toHaveBeenCalledWith('user-1', { tenantId: 'tenant-1', organizationId: 'org-1' })
213
+ expect(result.ok).toBe(false)
214
+ })
215
+
216
+ it('maps a custom operation to update so update-scoped guards run', async () => {
217
+ const updateGuard = makeGuard({
218
+ id: 'update-only-guard',
219
+ operations: ['update'],
220
+ validate: jest.fn().mockResolvedValue({ ok: true }),
221
+ })
222
+ const createGuard = makeGuard({
223
+ id: 'create-only-guard',
224
+ operations: ['create'],
225
+ validate: jest.fn().mockResolvedValue({ ok: true }),
226
+ })
227
+ registerStoreGuards([updateGuard, createGuard])
228
+
229
+ const result = await runRouteMutationGuards({
230
+ container: makeContainer(),
231
+ req: makeRequest(),
232
+ auth: baseAuth,
233
+ input: { ...baseInput, operation: 'custom' },
234
+ })
235
+
236
+ expect(result.ok).toBe(true)
237
+ expect(updateGuard.validate).toHaveBeenCalledTimes(1)
238
+ expect(createGuard.validate).not.toHaveBeenCalled()
239
+ })
240
+
241
+ it('includes the bridged legacy DI guard', async () => {
242
+ const legacyService = {
243
+ validateMutation: jest.fn().mockResolvedValue({ ok: false, status: 409, body: { error: 'locked' } }),
244
+ afterMutationSuccess: jest.fn().mockResolvedValue(undefined),
245
+ }
246
+
247
+ const result = await runRouteMutationGuards({
248
+ container: makeContainer({ crudMutationGuardService: legacyService }),
249
+ req: makeRequest('PUT'),
250
+ auth: baseAuth,
251
+ input: baseInput,
252
+ })
253
+
254
+ expect(legacyService.validateMutation).toHaveBeenCalledTimes(1)
255
+ expect(result.ok).toBe(false)
256
+ if (result.ok) throw new Error('expected blocked result')
257
+ expect(result.errorStatus).toBe(409)
258
+ expect(result.errorBody).toEqual({ error: 'locked' })
259
+ })
260
+ })
@@ -269,8 +269,8 @@ function buildCfDefIndexCacheKey(opts: {
269
269
  fieldsetKey: string | null
270
270
  }): string {
271
271
  const tenant = opts.tenantId ?? 'global'
272
- const entities = opts.entityIds.slice().sort().join('|')
273
- const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort().join('|') : 'none'
272
+ const entities = opts.entityIds.slice().sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join('|')
273
+ const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join('|') : 'none'
274
274
  const fieldset = opts.fieldsetKey ?? 'all'
275
275
  return `${CF_DEF_INDEX_CACHE_KEY_PREFIX}:${tenant}:${entities}:${orgs}:${fieldset}`
276
276
  }
@@ -295,7 +295,7 @@ function normalizeFieldsetKey(value: string | string[] | null | undefined): stri
295
295
  .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
296
296
  .filter((entry) => entry.length > 0)
297
297
  if (!cleaned.length) return null
298
- return cleaned.sort().join(',')
298
+ return cleaned.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(',')
299
299
  }
300
300
  if (typeof value !== 'string') return null
301
301
  const trimmed = value.trim()
@@ -58,8 +58,14 @@ function resolveCrudMutationGuardService(container: AwilixContainer): CrudMutati
58
58
  }
59
59
 
60
60
  /**
61
- * @deprecated Use `runMutationGuards()` from `@open-mercato/shared/lib/crud/mutation-guard-registry` instead.
62
- * This function is bridged internally via `bridgeLegacyGuard()` and will be removed in a future release.
61
+ * @deprecated Resolves ONLY the single DI-registered `crudMutationGuardService`,
62
+ * so it silently bypasses every guard in the global mutation-guard store
63
+ * (`getAllMutationGuardInstances()`). Use the full registry instead:
64
+ * `runRouteMutationGuards()` from `@open-mercato/shared/lib/crud/route-mutation-guard`
65
+ * for custom write routes, or `runMutationGuards()` from
66
+ * `@open-mercato/shared/lib/crud/mutation-guard-registry` directly. The legacy
67
+ * service is still honored on the modern path via `bridgeLegacyGuard()`. This
68
+ * function will be removed in a future release.
63
69
  */
64
70
  export async function validateCrudMutationGuard(
65
71
  container: AwilixContainer,
@@ -71,8 +77,13 @@ export async function validateCrudMutationGuard(
71
77
  }
72
78
 
73
79
  /**
74
- * @deprecated Use `runMutationGuards()` afterSuccess callbacks from `@open-mercato/shared/lib/crud/mutation-guard-registry` instead.
75
- * This function is bridged internally via `bridgeLegacyGuard()` and will be removed in a future release.
80
+ * @deprecated Runs ONLY the single DI-registered `crudMutationGuardService`'s
81
+ * after-success hook, skipping the registry guards' `afterSuccess` callbacks.
82
+ * Use the `runAfterSuccess()` returned by `runRouteMutationGuards()` from
83
+ * `@open-mercato/shared/lib/crud/route-mutation-guard`, or the
84
+ * `afterSuccessCallbacks` returned by `runMutationGuards()` from
85
+ * `@open-mercato/shared/lib/crud/mutation-guard-registry`. This function will be
86
+ * removed in a future release.
76
87
  */
77
88
  export async function runCrudMutationGuardAfterSuccess(
78
89
  container: AwilixContainer,
@@ -0,0 +1,181 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import {
3
+ bridgeLegacyGuard,
4
+ runMutationGuards,
5
+ type MutationGuard,
6
+ } from './mutation-guard-registry'
7
+ import { getAllMutationGuardInstances } from './mutation-guard-store'
8
+
9
+ /**
10
+ * Shared registry-based mutation-guard wrapper for custom write routes that do
11
+ * not use `makeCrudRoute`.
12
+ *
13
+ * This mirrors `collectAndRunGuards()` in `factory.ts`: it runs **every** guard
14
+ * collected from the global mutation-guard store (`getAllMutationGuardInstances()`)
15
+ * plus the bridged legacy DI service (`bridgeLegacyGuard()`), so a custom route
16
+ * enforces the same guard set as every `makeCrudRoute` write.
17
+ *
18
+ * Prefer this over the deprecated `validateCrudMutationGuard()` /
19
+ * `runCrudMutationGuardAfterSuccess()` pair, which resolve only the single
20
+ * DI-registered `crudMutationGuardService` and silently skip registry guards.
21
+ */
22
+
23
+ export type RouteMutationGuardOperation = 'create' | 'update' | 'delete' | 'custom'
24
+
25
+ export type RouteMutationGuardAuth = {
26
+ userId: string
27
+ tenantId: string
28
+ organizationId?: string | null
29
+ /**
30
+ * Pre-resolved granted features for the caller. When omitted, the wrapper
31
+ * resolves them via `rbacService.getGrantedFeatures` (same source the CRUD
32
+ * factory uses). Feature-gated registry guards only run when their required
33
+ * features are present, so supplying the wrong set silently skips guards.
34
+ */
35
+ userFeatures?: string[]
36
+ }
37
+
38
+ export type RouteMutationGuardInput = {
39
+ resourceKind: string
40
+ resourceId?: string | null
41
+ /**
42
+ * The route's logical operation. `'custom'` (state-changing action endpoints)
43
+ * is mapped to the closest registry operation, `'update'`, because
44
+ * `runMutationGuards` only understands `create | update | delete`.
45
+ */
46
+ operation?: RouteMutationGuardOperation
47
+ mutationPayload?: Record<string, unknown> | null
48
+ }
49
+
50
+ export type RouteMutationGuardBlocked = {
51
+ ok: false
52
+ errorStatus: number
53
+ errorBody: Record<string, unknown>
54
+ /** Ready-to-return JSON response built from `errorBody` / `errorStatus`. */
55
+ response: Response
56
+ }
57
+
58
+ export type RouteMutationGuardPassed = {
59
+ ok: true
60
+ /** Merged payload when a guard transformed it; `undefined` when unchanged. */
61
+ modifiedPayload?: Record<string, unknown>
62
+ /**
63
+ * Runs every guard's `afterSuccess` callback that requested it. Callback
64
+ * failures are caught and logged so a committed write still succeeds — call
65
+ * this only after the mutation has committed.
66
+ */
67
+ runAfterSuccess: () => Promise<void>
68
+ }
69
+
70
+ export type RouteMutationGuardResult = RouteMutationGuardBlocked | RouteMutationGuardPassed
71
+
72
+ type RbacServiceLike = {
73
+ getGrantedFeatures: (
74
+ userId: string,
75
+ opts: { tenantId: string | null; organizationId: string | null },
76
+ ) => Promise<string[]>
77
+ }
78
+
79
+ /**
80
+ * Map a route-level operation to the registry operation set. State-changing
81
+ * action endpoints (`'custom'`) and `'update'` both map to `'update'` per the
82
+ * `packages/core/AGENTS.md` → API Routes guidance.
83
+ */
84
+ export function toRegistryMutationOperation(
85
+ operation: RouteMutationGuardOperation | undefined,
86
+ ): 'create' | 'update' | 'delete' {
87
+ if (operation === 'create' || operation === 'delete') return operation
88
+ return 'update'
89
+ }
90
+
91
+ async function resolveRouteUserFeatures(
92
+ container: AwilixContainer,
93
+ auth: RouteMutationGuardAuth,
94
+ ): Promise<string[]> {
95
+ if (auth.userFeatures) return auth.userFeatures
96
+ try {
97
+ const rbac = container.resolve('rbacService') as RbacServiceLike | undefined
98
+ if (rbac?.getGrantedFeatures) {
99
+ return await rbac.getGrantedFeatures(auth.userId, {
100
+ tenantId: auth.tenantId,
101
+ organizationId: auth.organizationId ?? null,
102
+ })
103
+ }
104
+ } catch {
105
+ // rbacService not available — guards without feature requirements still run.
106
+ }
107
+ return []
108
+ }
109
+
110
+ /**
111
+ * Collect every registered mutation guard plus the bridged legacy DI service,
112
+ * run them against the route's mutation, and return either a blocked result
113
+ * (with a ready response) or a passed result carrying the merged payload and an
114
+ * after-success runner.
115
+ */
116
+ export async function runRouteMutationGuards(params: {
117
+ container: AwilixContainer
118
+ req: Request
119
+ auth: RouteMutationGuardAuth
120
+ input: RouteMutationGuardInput
121
+ }): Promise<RouteMutationGuardResult> {
122
+ const { container, req, auth, input } = params
123
+ const operation = toRegistryMutationOperation(input.operation)
124
+
125
+ const allGuards: MutationGuard[] = [...getAllMutationGuardInstances()]
126
+ const legacyGuard = bridgeLegacyGuard(container)
127
+ if (legacyGuard) allGuards.push(legacyGuard)
128
+
129
+ const userFeatures = await resolveRouteUserFeatures(container, auth)
130
+
131
+ const guardResult = await runMutationGuards(
132
+ allGuards,
133
+ {
134
+ tenantId: auth.tenantId,
135
+ organizationId: auth.organizationId ?? null,
136
+ userId: auth.userId,
137
+ resourceKind: input.resourceKind,
138
+ resourceId: input.resourceId ?? null,
139
+ operation,
140
+ requestMethod: req.method,
141
+ requestHeaders: req.headers,
142
+ mutationPayload: input.mutationPayload ?? null,
143
+ },
144
+ { userFeatures },
145
+ )
146
+
147
+ if (!guardResult.ok) {
148
+ const errorStatus = guardResult.errorStatus ?? 422
149
+ const errorBody = guardResult.errorBody ?? { error: 'Operation blocked by guard' }
150
+ return {
151
+ ok: false,
152
+ errorStatus,
153
+ errorBody,
154
+ response: Response.json(errorBody, { status: errorStatus }),
155
+ }
156
+ }
157
+
158
+ return {
159
+ ok: true,
160
+ modifiedPayload: guardResult.modifiedPayload,
161
+ runAfterSuccess: async () => {
162
+ for (const { guard, metadata } of guardResult.afterSuccessCallbacks) {
163
+ try {
164
+ await guard.afterSuccess!({
165
+ tenantId: auth.tenantId,
166
+ organizationId: auth.organizationId ?? null,
167
+ userId: auth.userId,
168
+ resourceKind: input.resourceKind,
169
+ resourceId: input.resourceId ?? '',
170
+ operation,
171
+ requestMethod: req.method,
172
+ requestHeaders: req.headers,
173
+ metadata,
174
+ })
175
+ } catch (error) {
176
+ console.error(`[mutation-guard] afterSuccess failed for guard ${guard.id}`, error)
177
+ }
178
+ }
179
+ },
180
+ }
181
+ }