@open-mercato/core 0.10.1-develop.7929.1.0822b5d5fd → 0.10.1-develop.7935.1.cc82ba7697

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.
@@ -119,11 +119,14 @@ async function requestOcrProcessing(em, attachment, driver, storagePath) {
119
119
  }
120
120
  const workerEm = em.fork();
121
121
  const maxWaitQueue = resolveOcrMaxWaitQueue();
122
- if (ocrWaitQueue.length + pendingOcrJobs >= maxWaitQueue) {
122
+ const freeOcrSlots = Math.max(0, resolveOcrMaxConcurrency() - activeOcrJobs);
123
+ const pendingWaiters = Math.max(0, pendingOcrJobs - freeOcrSlots);
124
+ if (ocrWaitQueue.length + pendingWaiters >= maxWaitQueue) {
123
125
  logger.warn("OCR wait queue full; falling back to inline text extraction", {
124
126
  attachmentId: attachment.id,
125
127
  waiting: ocrWaitQueue.length,
126
128
  pending: pendingOcrJobs,
129
+ pendingWaiters,
127
130
  maxWaitQueue
128
131
  });
129
132
  await persistInlineTextExtractionFallback(workerEm, payload, driver);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/attachments/lib/ocrQueue.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { OcrService } from './ocrService'\nimport type { StorageDriver } from './drivers/types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveOcrMaxConcurrency, resolveOcrMaxWaitQueue } from './ocrLimits'\nimport { extractAttachmentContent } from './textExtraction'\n\nconst logger = createLogger('attachments').child({ component: 'ocr' })\n\nexport type OcrRequestedEvent = {\n attachmentId: string\n storagePath: string\n mimeType: string\n partitionCode: string\n organizationId: string | null\n tenantId: string | null\n}\n\n/** Outcome of `requestOcrProcessing` \u2014 background LLM OCR vs inline text fallback. */\nexport type OcrProcessingDispatch = 'queued' | 'inline_fallback'\n\nlet activeOcrJobs = 0\nconst ocrWaitQueue: Array<() => void> = []\n// Jobs scheduled with setImmediate that have not yet entered a slot or the wait queue.\n// Counted toward the wait-queue cap so a burst in one event-loop turn cannot pass the check together.\nlet pendingOcrJobs = 0\n\n/** Test-only: reset in-process OCR concurrency bookkeeping. */\nexport function resetOcrConcurrencyStateForTests(): void {\n activeOcrJobs = 0\n ocrWaitQueue.length = 0\n pendingOcrJobs = 0\n}\n\n/** Test-only: inspect in-process OCR concurrency counters. */\nexport function getOcrConcurrencyStateForTests(): { active: number; waiting: number } {\n return { active: activeOcrJobs, waiting: ocrWaitQueue.length }\n}\n\nexport async function withOcrConcurrencySlot<T>(run: () => Promise<T>): Promise<T> {\n const maxConcurrency = resolveOcrMaxConcurrency()\n // Re-check after wake: multiple waiters can resume when one slot frees (barging).\n while (activeOcrJobs >= maxConcurrency) {\n await new Promise<void>((resolve) => {\n ocrWaitQueue.push(resolve)\n })\n }\n activeOcrJobs += 1\n try {\n return await run()\n } finally {\n activeOcrJobs -= 1\n const next = ocrWaitQueue.shift()\n if (next) next()\n }\n}\n\nexport async function processAttachmentOcr(\n em: EntityManager,\n payload: OcrRequestedEvent,\n driver: StorageDriver,\n): Promise<void> {\n const { attachmentId, storagePath, mimeType, partitionCode } = payload\n\n logger.info('Processing started', { attachmentId })\n const startTime = Date.now()\n\n const { filePath, cleanup } = await driver.toLocalPath(partitionCode, storagePath)\n try {\n const partition = await em.findOne(AttachmentPartition, { code: partitionCode })\n const resolvedModel = partition?.ocrModel ?? process.env.OCR_MODEL ?? 'gpt-4o'\n\n const ocrService = new OcrService()\n\n if (!ocrService.available) {\n logger.warn('OPENAI_API_KEY not configured, skipping OCR', { attachmentId })\n return\n }\n\n const result = await ocrService.processFile({\n filePath,\n mimeType,\n model: resolvedModel,\n })\n\n if (!result) {\n logger.info('No content extracted', { attachmentId })\n return\n }\n\n const attachment = await em.findOne(Attachment, { id: attachmentId })\n if (!attachment) {\n logger.error('Attachment not found', { attachmentId })\n return\n }\n\n attachment.content = result.content\n await em.persist(attachment).flush()\n\n logger.info('Processing completed', {\n attachmentId,\n pageCount: result.pageCount,\n contentLength: result.content.length,\n timeMs: result.processingTimeMs,\n totalTimeMs: Date.now() - startTime,\n })\n } catch (error) {\n logger.error('Processing failed', { attachmentId, err: error })\n } finally {\n await cleanup().catch((cleanupError) => {\n logger.warn('Temp file cleanup failed', { err: cleanupError })\n })\n }\n}\n\n/**\n * When the in-process OCR wait queue is full, extract plain text/PDF/DOCX content\n * inline (no LLM) so `attachment.content` is not left NULL forever.\n */\nasync function persistInlineTextExtractionFallback(\n em: EntityManager,\n payload: OcrRequestedEvent,\n driver: StorageDriver,\n): Promise<void> {\n const { attachmentId, storagePath, mimeType, partitionCode } = payload\n const { filePath, cleanup } = await driver.toLocalPath(partitionCode, storagePath)\n try {\n const content = await extractAttachmentContent({ filePath, mimeType })\n if (!content) {\n logger.info('OCR wait queue full; inline extraction produced no content', { attachmentId })\n return\n }\n const row = await em.findOne(Attachment, { id: attachmentId })\n if (!row) {\n logger.error('Attachment not found during OCR overflow fallback', { attachmentId })\n return\n }\n row.content = content\n await em.persist(row).flush()\n logger.info('OCR wait queue full; stored inline text extraction', {\n attachmentId,\n contentLength: content.length,\n })\n } catch (error) {\n logger.error('OCR wait queue overflow inline extraction failed', { attachmentId, err: error })\n } finally {\n await cleanup().catch((cleanupError) => {\n logger.warn('Temp file cleanup failed after OCR overflow fallback', { err: cleanupError })\n })\n }\n}\n\nexport async function requestOcrProcessing(\n em: EntityManager,\n attachment: Attachment,\n driver: StorageDriver,\n storagePath: string,\n): Promise<OcrProcessingDispatch> {\n const payload: OcrRequestedEvent = {\n attachmentId: attachment.id,\n storagePath,\n mimeType: attachment.mimeType,\n partitionCode: attachment.partitionCode,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n }\n\n if (typeof (em as { fork?: unknown })?.fork !== 'function') {\n throw new Error(\n '[internal] attachments OCR background processing requires an EntityManager that exposes fork(); ' +\n 'reusing the request-scoped EntityManager in the async worker would race with later request mutations.',\n )\n }\n\n const workerEm = em.fork()\n\n const maxWaitQueue = resolveOcrMaxWaitQueue()\n if (ocrWaitQueue.length + pendingOcrJobs >= maxWaitQueue) {\n logger.warn('OCR wait queue full; falling back to inline text extraction', {\n attachmentId: attachment.id,\n waiting: ocrWaitQueue.length,\n pending: pendingOcrJobs,\n maxWaitQueue,\n })\n await persistInlineTextExtractionFallback(workerEm, payload, driver)\n return 'inline_fallback'\n }\n\n pendingOcrJobs += 1\n setImmediate(() => {\n pendingOcrJobs -= 1\n withOcrConcurrencySlot(() => processAttachmentOcr(workerEm, payload, driver)).catch((error) => {\n logger.error('Background processing error', { err: error })\n })\n })\n return 'queued'\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { OcrService } from './ocrService'\nimport type { StorageDriver } from './drivers/types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveOcrMaxConcurrency, resolveOcrMaxWaitQueue } from './ocrLimits'\nimport { extractAttachmentContent } from './textExtraction'\n\nconst logger = createLogger('attachments').child({ component: 'ocr' })\n\nexport type OcrRequestedEvent = {\n attachmentId: string\n storagePath: string\n mimeType: string\n partitionCode: string\n organizationId: string | null\n tenantId: string | null\n}\n\n/** Outcome of `requestOcrProcessing` \u2014 background LLM OCR vs inline text fallback. */\nexport type OcrProcessingDispatch = 'queued' | 'inline_fallback'\n\nlet activeOcrJobs = 0\nconst ocrWaitQueue: Array<() => void> = []\n// Jobs scheduled with setImmediate that have not yet entered a slot or the wait queue.\nlet pendingOcrJobs = 0\n\n/** Test-only: reset in-process OCR concurrency bookkeeping. */\nexport function resetOcrConcurrencyStateForTests(): void {\n activeOcrJobs = 0\n ocrWaitQueue.length = 0\n pendingOcrJobs = 0\n}\n\n/** Test-only: inspect in-process OCR concurrency counters. */\nexport function getOcrConcurrencyStateForTests(): { active: number; waiting: number } {\n return { active: activeOcrJobs, waiting: ocrWaitQueue.length }\n}\n\nexport async function withOcrConcurrencySlot<T>(run: () => Promise<T>): Promise<T> {\n const maxConcurrency = resolveOcrMaxConcurrency()\n // Re-check after wake: multiple waiters can resume when one slot frees (barging).\n while (activeOcrJobs >= maxConcurrency) {\n await new Promise<void>((resolve) => {\n ocrWaitQueue.push(resolve)\n })\n }\n activeOcrJobs += 1\n try {\n return await run()\n } finally {\n activeOcrJobs -= 1\n const next = ocrWaitQueue.shift()\n if (next) next()\n }\n}\n\nexport async function processAttachmentOcr(\n em: EntityManager,\n payload: OcrRequestedEvent,\n driver: StorageDriver,\n): Promise<void> {\n const { attachmentId, storagePath, mimeType, partitionCode } = payload\n\n logger.info('Processing started', { attachmentId })\n const startTime = Date.now()\n\n const { filePath, cleanup } = await driver.toLocalPath(partitionCode, storagePath)\n try {\n const partition = await em.findOne(AttachmentPartition, { code: partitionCode })\n const resolvedModel = partition?.ocrModel ?? process.env.OCR_MODEL ?? 'gpt-4o'\n\n const ocrService = new OcrService()\n\n if (!ocrService.available) {\n logger.warn('OPENAI_API_KEY not configured, skipping OCR', { attachmentId })\n return\n }\n\n const result = await ocrService.processFile({\n filePath,\n mimeType,\n model: resolvedModel,\n })\n\n if (!result) {\n logger.info('No content extracted', { attachmentId })\n return\n }\n\n const attachment = await em.findOne(Attachment, { id: attachmentId })\n if (!attachment) {\n logger.error('Attachment not found', { attachmentId })\n return\n }\n\n attachment.content = result.content\n await em.persist(attachment).flush()\n\n logger.info('Processing completed', {\n attachmentId,\n pageCount: result.pageCount,\n contentLength: result.content.length,\n timeMs: result.processingTimeMs,\n totalTimeMs: Date.now() - startTime,\n })\n } catch (error) {\n logger.error('Processing failed', { attachmentId, err: error })\n } finally {\n await cleanup().catch((cleanupError) => {\n logger.warn('Temp file cleanup failed', { err: cleanupError })\n })\n }\n}\n\n/**\n * When the in-process OCR wait queue is full, extract plain text/PDF/DOCX content\n * inline (no LLM) so `attachment.content` is not left NULL forever.\n */\nasync function persistInlineTextExtractionFallback(\n em: EntityManager,\n payload: OcrRequestedEvent,\n driver: StorageDriver,\n): Promise<void> {\n const { attachmentId, storagePath, mimeType, partitionCode } = payload\n const { filePath, cleanup } = await driver.toLocalPath(partitionCode, storagePath)\n try {\n const content = await extractAttachmentContent({ filePath, mimeType })\n if (!content) {\n logger.info('OCR wait queue full; inline extraction produced no content', { attachmentId })\n return\n }\n const row = await em.findOne(Attachment, { id: attachmentId })\n if (!row) {\n logger.error('Attachment not found during OCR overflow fallback', { attachmentId })\n return\n }\n row.content = content\n await em.persist(row).flush()\n logger.info('OCR wait queue full; stored inline text extraction', {\n attachmentId,\n contentLength: content.length,\n })\n } catch (error) {\n logger.error('OCR wait queue overflow inline extraction failed', { attachmentId, err: error })\n } finally {\n await cleanup().catch((cleanupError) => {\n logger.warn('Temp file cleanup failed after OCR overflow fallback', { err: cleanupError })\n })\n }\n}\n\nexport async function requestOcrProcessing(\n em: EntityManager,\n attachment: Attachment,\n driver: StorageDriver,\n storagePath: string,\n): Promise<OcrProcessingDispatch> {\n const payload: OcrRequestedEvent = {\n attachmentId: attachment.id,\n storagePath,\n mimeType: attachment.mimeType,\n partitionCode: attachment.partitionCode,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n }\n\n if (typeof (em as { fork?: unknown })?.fork !== 'function') {\n throw new Error(\n '[internal] attachments OCR background processing requires an EntityManager that exposes fork(); ' +\n 'reusing the request-scoped EntityManager in the async worker would race with later request mutations.',\n )\n }\n\n const workerEm = em.fork()\n\n const maxWaitQueue = resolveOcrMaxWaitQueue()\n const freeOcrSlots = Math.max(0, resolveOcrMaxConcurrency() - activeOcrJobs)\n const pendingWaiters = Math.max(0, pendingOcrJobs - freeOcrSlots)\n if (ocrWaitQueue.length + pendingWaiters >= maxWaitQueue) {\n logger.warn('OCR wait queue full; falling back to inline text extraction', {\n attachmentId: attachment.id,\n waiting: ocrWaitQueue.length,\n pending: pendingOcrJobs,\n pendingWaiters,\n maxWaitQueue,\n })\n await persistInlineTextExtractionFallback(workerEm, payload, driver)\n return 'inline_fallback'\n }\n\n pendingOcrJobs += 1\n setImmediate(() => {\n pendingOcrJobs -= 1\n withOcrConcurrencySlot(() => processAttachmentOcr(workerEm, payload, driver)).catch((error) => {\n logger.error('Background processing error', { err: error })\n })\n })\n return 'queued'\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,7 +7,7 @@ import {
7
7
  buildChanges,
8
8
  requireId
9
9
  } from "@open-mercato/shared/lib/commands/helpers";
10
- import { CrudHttpError, forbidden } from "@open-mercato/shared/lib/crud/errors";
10
+ import { CrudHttpError, forbidden, isUniqueViolation } from "@open-mercato/shared/lib/crud/errors";
11
11
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
12
12
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
13
13
  import { z } from "zod";
@@ -137,13 +137,25 @@ const createRoleCommand = {
137
137
  throw new CrudHttpError(400, { error: "tenantId is required \u2014 global roles are not supported" });
138
138
  }
139
139
  const de = ctx.container.resolve("dataEngine");
140
- const role = await de.createOrmEntity({
141
- entity: Role,
142
- data: {
143
- name: parsed.name,
144
- tenantId: resolvedTenantId
145
- }
146
- });
140
+ let role;
141
+ try {
142
+ role = await de.createOrmEntity({
143
+ entity: Role,
144
+ data: {
145
+ name: parsed.name,
146
+ tenantId: resolvedTenantId
147
+ }
148
+ });
149
+ } catch (error) {
150
+ if (!isUniqueViolation(error, "roles_tenant_id_name_unique")) throw error;
151
+ const { translate } = await resolveTranslations();
152
+ const message = translate("auth.roles.errors.nameExists", "A role with this name already exists");
153
+ throw new CrudHttpError(400, {
154
+ error: message,
155
+ fieldErrors: { name: message },
156
+ details: [{ path: ["name"], message, code: "duplicate", origin: "validation" }]
157
+ });
158
+ }
147
159
  await setCustomFieldsIfAny({
148
160
  dataEngine: de,
149
161
  entityId: E.auth.role,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/auth/commands/roles.ts"],
4
- "sourcesContent": ["import type { CommandHandler, CommandReplayAuthorizationArgs, CommandUndoLogEntry } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n parseWithCustomFields,\n setCustomFieldsIfAny,\n emitCrudSideEffects,\n emitCrudUndoSideEffects,\n buildChanges,\n requireId,\n} from '@open-mercato/shared/lib/commands/helpers'\nimport type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { Role, RoleAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport {\n loadCustomFieldSnapshot,\n buildCustomFieldResetMap,\n diffCustomFieldChanges,\n} from '@open-mercato/shared/lib/commands/customFieldSnapshots'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { resolveIsSuperAdmin, normalizeTenantId } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport {\n assertActorCanAccessRoleTarget,\n assertActorCanGrantAcl,\n assertActorCanModifySuperAdminRoleTarget,\n} from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n assertReplaySnapshotMatches,\n extractStoredReplayInput,\n lockReplayAuthorizationState,\n rerunReplayTransactionGuardAfterLocks,\n requireCurrentReplayFeature,\n requireCurrentReplaySuperAdmin,\n requireTransactionalReplayEntityManager,\n} from '@open-mercato/core/modules/auth/lib/commandReplay'\nimport { lockRoleWriterAuthorizationState } from '@open-mercato/core/modules/auth/lib/authorizationStateLocks'\n\ntype SerializedRole = {\n name: string\n tenantId: string\n custom?: Record<string, unknown>\n}\n\ntype RoleAclSnapshot = {\n id: string | null\n tenantId: string\n features: string[] | null\n isSuperAdmin: boolean\n organizations: string[] | null\n}\n\ntype RoleUndoSnapshot = {\n id: string\n name: string\n tenantId: string\n acls: RoleAclSnapshot[]\n custom?: Record<string, unknown>\n}\n\ntype RoleSnapshots = {\n view: SerializedRole\n undo: RoleUndoSnapshot\n}\n\nconst ROLE_REDO_LOG = Symbol('auth.roles.redoLog')\n\ntype RoleReplayRuntimeContext = CommandRuntimeContext & {\n [ROLE_REDO_LOG]?: CommandUndoLogEntry\n}\n\nfunction withRoleRedoLog(ctx: CommandRuntimeContext, logEntry: CommandUndoLogEntry): RoleReplayRuntimeContext {\n return { ...ctx, [ROLE_REDO_LOG]: logEntry }\n}\n\nfunction resolveCommandEm(ctx: CommandRuntimeContext, forkWhenUnbound = false): EntityManager {\n if (ctx.transactionalEm) return ctx.transactionalEm\n const em = ctx.container.resolve('em') as EntityManager\n return forkWhenUnbound ? em.fork() : em\n}\n\nfunction resolveActorTenantScope(ctx: CommandRuntimeContext): string | null {\n if (ctx.systemActor === true) return null\n const auth = ctx.auth\n if (!auth) return null\n if ((auth as { isSuperAdmin?: boolean }).isSuperAdmin === true) return null\n return normalizeTenantId(auth.tenantId ?? null) ?? null\n}\n\nfunction assertRoleTenantInScope(actorTenantScope: string | null, targetTenantId: unknown): void {\n if (!actorTenantScope) return\n const targetTenant = normalizeTenantId(targetTenantId) ?? null\n if (!targetTenant || targetTenant !== actorTenantScope) {\n throw new CrudHttpError(404, { error: 'Role not found' })\n }\n}\n\nconst RESERVED_ROLE_NAMES = new Set(['superadmin', 'admin'])\n\nfunction isReservedRoleName(name: string | undefined | null): boolean {\n if (typeof name !== 'string') return false\n const normalized = name.trim().toLowerCase()\n return normalized.length > 0 && RESERVED_ROLE_NAMES.has(normalized)\n}\n\nfunction assertRoleNameAllowed(name: string | undefined | null) {\n if (isReservedRoleName(name)) {\n throw new CrudHttpError(400, { error: 'Role name is reserved' })\n }\n}\n\ntype ResolvedActorScope = { isSuperAdmin: boolean; actorTenantId: string | null }\n\nasync function resolveActorScope(ctx: { auth: { tenantId?: string | null; sub?: string | null; orgId?: string | null; isSuperAdmin?: boolean } | null; container: { resolve<T = unknown>(name: string): T } }): Promise<ResolvedActorScope> {\n const isSuperAdmin = await resolveIsSuperAdmin(ctx)\n const actorTenantId = normalizeTenantId(ctx.auth?.tenantId ?? null) ?? null\n return { isSuperAdmin, actorTenantId }\n}\n\nfunction buildScopedRoleFilter(roleId: string, scope: ResolvedActorScope): FilterQuery<Role> {\n const filter: FilterQuery<Role> = { id: roleId, deletedAt: null }\n if (!scope.isSuperAdmin) {\n ;(filter as Record<string, unknown>).tenantId = scope.actorTenantId\n }\n return filter\n}\n\nconst createSchema = z.object({\n name: z.string().min(2).max(100),\n tenantId: z.string().uuid().optional(),\n})\n\nconst updateSchema = z.object({\n id: z.string().uuid(),\n name: z.string().min(2).max(100).optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const roleCrudEvents: CrudEventsConfig = {\n module: 'auth',\n entity: 'role',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\nexport const roleCrudIndexer: CrudIndexerConfig = {\n entityType: E.auth.role,\n buildUpsertPayload: (ctx) => ({\n entityType: E.auth.role,\n recordId: ctx.identifiers.id,\n tenantId: ctx.identifiers.tenantId,\n }),\n buildDeletePayload: (ctx) => ({\n entityType: E.auth.role,\n recordId: ctx.identifiers.id,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\nconst createRoleCommand: CommandHandler<Record<string, unknown>, Role> = {\n id: 'auth.roles.create',\n atomicReplay: true,\n stabilizeReplay: stabilizeRoleReplay,\n authorizeReplay: (params) => authorizeRoleReplay('create', params),\n async execute(rawInput, ctx) {\n const rawBody = rawInput && typeof rawInput === 'object' ? rawInput as Record<string, unknown> : {}\n if ('tenantId' in rawBody && rawBody.tenantId === null) {\n throw new CrudHttpError(400, { error: 'tenantId cannot be null \u2014 global roles are not supported' })\n }\n const { parsed, custom } = parseWithCustomFields(createSchema, rawInput)\n assertRoleNameAllowed(parsed.name)\n const scope = await resolveActorScope(ctx)\n const requestedTenantId = parsed.tenantId ?? null\n if (!scope.isSuperAdmin && requestedTenantId && requestedTenantId !== scope.actorTenantId) {\n throw new CrudHttpError(403, { error: 'Not authorized to target this tenant.' })\n }\n const resolvedTenantId = scope.isSuperAdmin\n ? (requestedTenantId ?? scope.actorTenantId)\n : scope.actorTenantId\n if (!resolvedTenantId) {\n throw new CrudHttpError(400, { error: 'tenantId is required \u2014 global roles are not supported' })\n }\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n const role = await de.createOrmEntity({\n entity: Role,\n data: {\n name: parsed.name,\n tenantId: resolvedTenantId,\n },\n })\n\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: String(role.id),\n organizationId: null,\n tenantId: resolvedTenantId,\n values: custom,\n })\n\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: role,\n identifiers: {\n id: String(role.id),\n organizationId: null,\n tenantId: resolvedTenantId,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n\n return role\n },\n captureAfter: async (_input, result, ctx) => {\n const em = resolveCommandEm(ctx, true)\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n })\n return serializeRole(result, custom)\n },\n buildLog: async ({ result, ctx }) => {\n const { translate } = await resolveTranslations()\n const em = resolveCommandEm(ctx, true)\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n })\n const snapshot = captureRoleSnapshots(result, [], custom)\n return {\n actionLabel: translate('auth.audit.roles.create', 'Create role'),\n resourceKind: 'auth.role',\n resourceId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n snapshotAfter: snapshot.view,\n payload: {\n undo: {\n after: snapshot.undo,\n },\n },\n }\n },\n undo: async ({ input, logEntry, ctx }) => {\n const undo = extractUndoPayload<RoleUndoPayload>(logEntry)?.after\n if (!undo) return\n const em = resolveCommandEm(ctx)\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await withAtomicFlush(em, [async () => {\n await authorizeRoleReplayAtMutation('create', {\n operation: 'undo',\n input,\n ctx,\n logEntry,\n })\n await lockRoleWriterAuthorizationState(em, [undo.id])\n await em.nativeDelete(RoleAcl, { role: undo.id as unknown as Role })\n if (undo.custom && Object.keys(undo.custom).length) {\n const reset = buildCustomFieldResetMap(undefined, undo.custom)\n if (Object.keys(reset).length) {\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: undo.id,\n organizationId: null,\n tenantId: undo.tenantId ?? null,\n values: reset,\n notify: false,\n })\n }\n }\n await de.deleteOrmEntity({\n entity: Role,\n where: { id: undo.id, deletedAt: null } as FilterQuery<Role>,\n soft: false,\n })\n }], { transaction: true, label: 'auth.roles.create.undo' })\n },\n redo: async ({ input, logEntry, ctx }) => {\n const after = resolveRedoSnapshot<RoleUndoSnapshot>(logEntry)\n if (!after) throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for role create' })\n const em = resolveCommandEm(ctx)\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n let role: Role | null = null\n await withAtomicFlush(em, [async () => {\n await authorizeRoleReplayAtMutation('create', {\n operation: 'redo',\n input,\n ctx,\n logEntry,\n })\n role = await findOneWithDecryption(em, Role, { id: after.id }, {}, { tenantId: null, organizationId: null })\n if (role) {\n role.deletedAt = null\n role.name = after.name\n role.tenantId = after.tenantId\n await em.flush()\n } else {\n role = await de.createOrmEntity({\n entity: Role,\n data: {\n id: after.id,\n name: after.name,\n tenantId: after.tenantId,\n },\n })\n }\n await restoreRoleAcls(em, after.id, after.acls)\n if (after.custom && Object.keys(after.custom).length) {\n const reset = buildCustomFieldResetMap(after.custom, undefined)\n if (Object.keys(reset).length) {\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: after.id,\n organizationId: null,\n tenantId: after.tenantId ?? null,\n values: reset,\n notify: false,\n })\n }\n }\n }], { transaction: true, label: 'auth.roles.create.redo' })\n if (!role) throw new CrudHttpError(400, { error: '[internal] redo failed to restore role row' })\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: role,\n identifiers: {\n id: after.id,\n organizationId: null,\n tenantId: after.tenantId ?? null,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n return role\n },\n}\n\nconst updateRoleCommand: CommandHandler<Record<string, unknown>, Role> = {\n id: 'auth.roles.update',\n atomicReplay: true,\n stabilizeReplay: stabilizeRoleReplay,\n authorizeReplay: (params) => authorizeRoleReplay('update', params),\n async prepare(rawInput, ctx) {\n const { parsed } = parseWithCustomFields(updateSchema, rawInput)\n const em = resolveCommandEm(ctx)\n const scope = await resolveActorScope(ctx)\n const existing = await findOneWithDecryption(em, Role, buildScopedRoleFilter(parsed.id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null })\n if (!existing) throw new CrudHttpError(404, { error: 'Role not found' })\n assertRoleTenantInScope(resolveActorTenantScope(ctx), existing.tenantId)\n const acls = await loadRoleAclSnapshots(em, parsed.id)\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: parsed.id,\n tenantId: existing.tenantId ? String(existing.tenantId) : null,\n })\n return { before: captureRoleSnapshots(existing, acls, custom) }\n },\n async execute(rawInput, ctx) {\n const { parsed, custom } = parseWithCustomFields(updateSchema, rawInput)\n const em = resolveCommandEm(ctx)\n const scope = await resolveActorScope(ctx)\n const current = await findOneWithDecryption(em, Role, buildScopedRoleFilter(parsed.id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null })\n if (!current) throw new CrudHttpError(404, { error: 'Role not found' })\n const actorTenantScope = resolveActorTenantScope(ctx)\n assertRoleTenantInScope(actorTenantScope, current.tenantId)\n if (parsed.name !== undefined) {\n const nextName = parsed.name\n if (nextName !== current.name) assertRoleNameAllowed(nextName)\n if (nextName !== current.name) {\n const assignments = await em.count(UserRole, { role: current, deletedAt: null })\n if (assignments > 0) {\n throw new CrudHttpError(400, { error: 'Role name cannot be changed while users are assigned' })\n }\n }\n }\n const wantsTenantChange = parsed.tenantId !== undefined && parsed.tenantId !== current.tenantId\n if (wantsTenantChange) {\n if (!scope.isSuperAdmin) {\n throw new CrudHttpError(403, { error: 'Not authorized to target this tenant.' })\n }\n const assignments = await em.count(UserRole, { role: current, deletedAt: null })\n if (assignments > 0) {\n throw new CrudHttpError(400, { error: 'Role cannot be moved to another tenant while users are assigned' })\n }\n }\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n const redoLogEntry = (ctx as RoleReplayRuntimeContext)[ROLE_REDO_LOG]\n let role: Role | null = null\n await withAtomicFlush(em, [async () => {\n if (redoLogEntry) {\n await authorizeRoleReplayAtMutation('update', {\n operation: 'redo',\n input: rawInput,\n ctx,\n logEntry: redoLogEntry,\n })\n }\n await lockRoleWriterAuthorizationState(em, [parsed.id])\n if (wantsTenantChange) {\n await em.nativeDelete(RoleAcl, { role: parsed.id as unknown as Role })\n }\n role = await de.updateOrmEntity({\n entity: Role,\n where: buildScopedRoleFilter(parsed.id, scope),\n apply: (entity) => {\n if (parsed.name !== undefined) entity.name = parsed.name\n if (parsed.tenantId !== undefined && scope.isSuperAdmin) entity.tenantId = parsed.tenantId\n },\n })\n if (!role) throw new CrudHttpError(404, { error: 'Role not found' })\n\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: String(role.id),\n organizationId: null,\n tenantId: role.tenantId ? String(role.tenantId) : null,\n values: custom,\n })\n }], {\n transaction: true,\n label: redoLogEntry ? 'auth.roles.update.redo' : 'auth.roles.update',\n })\n const updatedRole = role as Role | null\n if (!updatedRole) throw new CrudHttpError(404, { error: 'Role not found' })\n\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: updatedRole,\n identifiers: {\n id: String(updatedRole.id),\n organizationId: null,\n tenantId: updatedRole.tenantId ? String(updatedRole.tenantId) : null,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n\n return updatedRole\n },\n captureAfter: async (_input, result, ctx) => {\n const em = resolveCommandEm(ctx, true)\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n })\n return serializeRole(result, custom)\n },\n buildLog: async ({ result, snapshots, ctx }) => {\n const { translate } = await resolveTranslations()\n const beforeSnapshots = snapshots.before as RoleSnapshots | undefined\n const before = beforeSnapshots?.view\n const beforeUndo = beforeSnapshots?.undo ?? null\n const em = resolveCommandEm(ctx, true)\n const afterAcls = await loadRoleAclSnapshots(em, String(result.id))\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n })\n const afterSnapshots = captureRoleSnapshots(result, afterAcls, custom)\n const after = afterSnapshots.view\n const changes = buildChanges(before ?? null, after as Record<string, unknown>, ['name', 'tenantId'])\n const customDiff = diffCustomFieldChanges(before?.custom, custom)\n for (const [key, diff] of Object.entries(customDiff)) {\n changes[`cf_${key}`] = diff\n }\n return {\n actionLabel: translate('auth.audit.roles.update', 'Update role'),\n resourceKind: 'auth.role',\n resourceId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n changes,\n snapshotBefore: before ?? null,\n snapshotAfter: after,\n payload: {\n undo: {\n before: beforeUndo,\n after: afterSnapshots.undo,\n },\n },\n }\n },\n undo: async ({ input, logEntry, ctx }) => {\n const undo = extractUndoPayload<RoleUndoPayload>(logEntry)\n const before = undo?.before\n const after = undo?.after\n if (!before) return\n const storedReplayInput = extractStoredReplayInput(logEntry.commandPayload, logEntry)\n const replayInput = Object.keys(storedReplayInput).length\n ? storedReplayInput\n : deriveRoleUpdateReplayInput(before, after ?? null)\n const em = resolveCommandEm(ctx)\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n let updated: Role | null = null\n await withAtomicFlush(em, [async () => {\n await authorizeRoleReplayAtMutation('update', {\n operation: 'undo',\n input,\n ctx,\n logEntry,\n })\n updated = await de.updateOrmEntity({\n entity: Role,\n where: { id: before.id, deletedAt: null } as FilterQuery<Role>,\n apply: (entity) => {\n if ('name' in replayInput) entity.name = before.name\n if ('tenantId' in replayInput) entity.tenantId = before.tenantId\n },\n })\n const restoresAcls = 'tenantId' in replayInput && after?.tenantId !== before.tenantId\n if (updated && restoresAcls) {\n await restoreRoleAcls(em, before.id, before.acls)\n }\n const reset = buildCustomFieldResetMap(before.custom, after?.custom)\n if (Object.keys(reset).length) {\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: before.id,\n organizationId: null,\n tenantId: before.tenantId,\n values: reset,\n notify: false,\n })\n }\n }], { transaction: true, label: 'auth.roles.update.undo' })\n await emitCrudUndoSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: updated,\n identifiers: {\n id: before.id,\n organizationId: null,\n tenantId: before.tenantId ?? null,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n },\n redo: ({ input, ctx, logEntry }) => updateRoleCommand.execute(input, withRoleRedoLog(ctx, logEntry)),\n}\n\nconst deleteRoleCommand: CommandHandler<{ body?: Record<string, unknown>; query?: Record<string, unknown> }, Role> = {\n id: 'auth.roles.delete',\n atomicReplay: true,\n stabilizeReplay: stabilizeRoleReplay,\n authorizeReplay: (params) => authorizeRoleReplay('delete', params),\n async prepare(input, ctx) {\n const id = requireId(input, 'Role id required')\n const em = resolveCommandEm(ctx)\n const scope = await resolveActorScope(ctx)\n const existing = await findOneWithDecryption(em, Role, buildScopedRoleFilter(id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null })\n if (!existing) return {}\n const actorTenantScope = resolveActorTenantScope(ctx)\n if (actorTenantScope) {\n const targetTenant = normalizeTenantId(existing.tenantId) ?? null\n if (!targetTenant || targetTenant !== actorTenantScope) return {}\n }\n const acls = await loadRoleAclSnapshots(em, id)\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: id,\n tenantId: existing.tenantId ? String(existing.tenantId) : null,\n })\n return { before: captureRoleSnapshots(existing, acls, custom) }\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Role id required')\n const em = resolveCommandEm(ctx)\n const scope = await resolveActorScope(ctx)\n const role = await findOneWithDecryption(em, Role, buildScopedRoleFilter(id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null })\n if (!role) throw new CrudHttpError(404, { error: 'Role not found' })\n assertRoleTenantInScope(resolveActorTenantScope(ctx), role.tenantId)\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n const redoLogEntry = (ctx as RoleReplayRuntimeContext)[ROLE_REDO_LOG]\n let deleted: Role | null = null\n await withAtomicFlush(em, [async () => {\n if (redoLogEntry) {\n await authorizeRoleReplayAtMutation('delete', {\n operation: 'redo',\n input,\n ctx,\n logEntry: redoLogEntry,\n })\n }\n await lockRoleWriterAuthorizationState(em, [id])\n const activeAssignments = await em.count(UserRole, { role, deletedAt: null })\n if (activeAssignments > 0) throw new CrudHttpError(400, { error: 'Role has assigned users' })\n await em.nativeDelete(RoleAcl, { role: id })\n deleted = await de.deleteOrmEntity({\n entity: Role,\n where: buildScopedRoleFilter(id, scope),\n soft: false,\n })\n if (!deleted) throw new CrudHttpError(404, { error: 'Role not found' })\n }], {\n transaction: true,\n label: redoLogEntry ? 'auth.roles.delete.redo' : 'auth.roles.delete',\n })\n const deletedRole = deleted as Role | null\n if (!deletedRole) throw new CrudHttpError(404, { error: 'Role not found' })\n\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'deleted',\n entity: deletedRole,\n identifiers: {\n id,\n organizationId: null,\n tenantId: deletedRole.tenantId ? String(deletedRole.tenantId) : null,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n\n return deletedRole\n },\n buildLog: async ({ snapshots, input }) => {\n const { translate } = await resolveTranslations()\n const beforeSnapshots = snapshots.before as RoleSnapshots | undefined\n const before = beforeSnapshots?.view\n const beforeUndo = beforeSnapshots?.undo ?? null\n const id = requireId(input, 'Role id required')\n return {\n actionLabel: translate('auth.audit.roles.delete', 'Delete role'),\n resourceKind: 'auth.role',\n resourceId: id,\n tenantId: before?.tenantId ?? null,\n snapshotBefore: before ?? null,\n payload: {\n undo: {\n before: beforeUndo,\n },\n },\n }\n },\n undo: async ({ input, logEntry, ctx }) => {\n const before = extractUndoPayload<RoleUndoPayload>(logEntry)?.before\n if (!before) return\n const em = resolveCommandEm(ctx)\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n let role: Role | null = null\n await withAtomicFlush(em, [async () => {\n await authorizeRoleReplayAtMutation('delete', {\n operation: 'undo',\n input,\n ctx,\n logEntry,\n })\n role = await findOneWithDecryption(em, Role, { id: before.id }, {}, { tenantId: null, organizationId: null })\n if (role) {\n role.deletedAt = null\n role.name = before.name\n role.tenantId = before.tenantId\n await em.flush()\n } else {\n role = await de.createOrmEntity({\n entity: Role,\n data: {\n id: before.id,\n name: before.name,\n tenantId: before.tenantId,\n },\n })\n }\n await restoreRoleAcls(em, before.id, before.acls)\n const reset = buildCustomFieldResetMap(before.custom, undefined)\n if (Object.keys(reset).length) {\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: before.id,\n organizationId: null,\n tenantId: before.tenantId ?? null,\n values: reset,\n notify: false,\n })\n }\n }], { transaction: true, label: 'auth.roles.delete.undo' })\n if (!role) throw new CrudHttpError(400, { error: '[internal] undo failed to restore role row' })\n await emitCrudUndoSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: role,\n identifiers: {\n id: before.id,\n organizationId: null,\n tenantId: before.tenantId ?? null,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n },\n redo: ({ input, ctx, logEntry }) => deleteRoleCommand.execute(input, withRoleRedoLog(ctx, logEntry)),\n}\n\ntype RoleReplayCommandKind = 'create' | 'update' | 'delete'\n\nfunction normalizeRoleAclSnapshots(acls: RoleAclSnapshot[]): RoleAclSnapshot[] {\n return acls\n .map((acl) => ({\n ...acl,\n features: acl.features ? [...acl.features].sort((left, right) => left.localeCompare(right)) : null,\n organizations: acl.organizations ? [...acl.organizations].sort((left, right) => left.localeCompare(right)) : null,\n }))\n .sort((left, right) => {\n const tenantOrder = left.tenantId.localeCompare(right.tenantId)\n if (tenantOrder !== 0) return tenantOrder\n return (left.id ?? '').localeCompare(right.id ?? '')\n })\n}\n\nfunction normalizeRoleReplaySnapshot(snapshot: RoleUndoSnapshot): RoleUndoSnapshot {\n return {\n ...snapshot,\n acls: normalizeRoleAclSnapshots(snapshot.acls),\n }\n}\n\nfunction toRoleUpdateReplayState(snapshot: RoleUndoSnapshot): SerializedRole {\n return {\n name: snapshot.name,\n tenantId: snapshot.tenantId,\n ...(snapshot.custom && Object.keys(snapshot.custom).length ? { custom: snapshot.custom } : {}),\n }\n}\n\nfunction deriveRoleUpdateReplayInput(\n before: RoleUndoSnapshot | null,\n after: RoleUndoSnapshot | null,\n): Record<string, unknown> {\n if (!before) return {}\n if (!after) return { id: before.id, name: before.name, tenantId: before.tenantId }\n const input: Record<string, unknown> = { id: after.id }\n if (before.name !== after.name) input.name = after.name\n if (before.tenantId !== after.tenantId) input.tenantId = after.tenantId\n return input\n}\n\nasync function authorizeRoleReplay(\n commandKind: RoleReplayCommandKind,\n params: CommandReplayAuthorizationArgs<unknown>,\n): Promise<void> {\n const { operation, ctx, logEntry } = params\n const em = requireTransactionalReplayEntityManager(ctx)\n await requireCurrentReplayFeature(ctx, 'auth.roles.manage')\n\n const undoPayload = extractUndoPayload<RoleUndoPayload>(logEntry)\n const before = undoPayload?.before ?? null\n const after = undoPayload?.after ?? null\n const roleId = logEntry.resourceId ?? before?.id ?? after?.id ?? null\n if (!roleId) {\n await assertReplaySnapshotMatches(null, { required: 'role replay snapshot' })\n return\n }\n\n const currentRole = await findOneWithDecryption(\n em,\n Role,\n { id: roleId, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n const rbacService = ctx.systemActor === true\n ? null\n : ctx.container.resolve('rbacService') as RbacService\n if (rbacService && currentRole) {\n await assertActorCanAccessRoleTarget({\n em,\n rbacService,\n requireBoundRbac: true,\n actorUserId: ctx.auth?.sub,\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.auth?.orgId ?? null,\n targetRoleId: roleId,\n })\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService,\n requireBoundRbac: true,\n actorUserId: ctx.auth?.sub,\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.auth?.orgId ?? null,\n targetRoleId: roleId,\n })\n }\n\n let currentSnapshot: RoleUndoSnapshot | null = null\n if (currentRole) {\n const currentTenantId = currentRole.tenantId ? String(currentRole.tenantId) : null\n const [acls, custom] = await Promise.all([\n loadRoleAclSnapshots(em, roleId),\n loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: roleId,\n tenantId: currentTenantId,\n }),\n ])\n currentSnapshot = captureRoleSnapshots(currentRole, acls, custom).undo\n }\n\n const expectedCurrent = operation === 'undo'\n ? commandKind === 'create' ? after : commandKind === 'update' ? after : null\n : commandKind === 'create' ? null : before\n const storedReplayInput = extractStoredReplayInput(params.input, logEntry)\n const replayInput = Object.keys(storedReplayInput).length\n ? storedReplayInput\n : deriveRoleUpdateReplayInput(before, after)\n const movesTenant = commandKind === 'update'\n && 'tenantId' in replayInput\n && before?.tenantId !== after?.tenantId\n if (movesTenant) {\n await requireCurrentReplaySuperAdmin(ctx)\n }\n\n if (commandKind === 'update' && !movesTenant) {\n if (!currentSnapshot || !expectedCurrent) {\n await assertReplaySnapshotMatches(currentSnapshot, expectedCurrent)\n } else {\n await assertReplaySnapshotMatches(\n toRoleUpdateReplayState(currentSnapshot),\n toRoleUpdateReplayState(expectedCurrent),\n )\n }\n } else {\n await assertReplaySnapshotMatches(\n currentSnapshot ? normalizeRoleReplaySnapshot(currentSnapshot) : null,\n expectedCurrent ? normalizeRoleReplaySnapshot(expectedCurrent) : null,\n )\n }\n\n const desiredState = operation === 'undo'\n ? commandKind === 'create' ? null : before\n : commandKind === 'delete' ? null : after\n if (desiredState && !currentRole && rbacService) {\n const actorUserId = ctx.auth?.sub\n if (!actorUserId) throw forbidden()\n const actorAcl = await rbacService.loadAclWithEntityManager(em, actorUserId, {\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.auth?.orgId ?? null,\n })\n if (!actorAcl.isSuperAdmin) assertRoleTenantInScope(ctx.auth?.tenantId ?? null, desiredState.tenantId)\n }\n const restoresAcls = Boolean(\n desiredState\n && desiredState.acls.length > 0\n && (commandKind !== 'update' || movesTenant),\n )\n if (!restoresAcls || !desiredState || !rbacService) return\n\n await requireCurrentReplayFeature(ctx, 'auth.acl.manage')\n for (const acl of desiredState.acls) {\n await assertActorCanGrantAcl({\n em,\n rbacService,\n requireBoundRbac: true,\n actorUserId: ctx.auth?.sub,\n tenantId: acl.tenantId,\n organizationId: null,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n organizations: acl.organizations,\n })\n }\n}\n\nasync function authorizeRoleReplayAtMutation(\n commandKind: RoleReplayCommandKind,\n params: CommandReplayAuthorizationArgs<unknown>,\n): Promise<void> {\n const em = params.ctx.transactionalEm\n ?? params.ctx.container.resolve('em') as EntityManager\n const boundParams = params.ctx.transactionalEm\n ? params\n : { ...params, ctx: { ...params.ctx, transactionalEm: em } }\n await stabilizeRoleReplay(boundParams)\n await rerunReplayTransactionGuardAfterLocks(boundParams, em)\n await authorizeRoleReplay(commandKind, boundParams)\n}\n\nasync function stabilizeRoleReplay(\n params: CommandReplayAuthorizationArgs<unknown>,\n): Promise<void> {\n const undoPayload = extractUndoPayload<RoleUndoPayload>(params.logEntry)\n const before = undoPayload?.before ?? null\n const after = undoPayload?.after ?? null\n const targetRoleId = params.logEntry.resourceId ?? before?.id ?? after?.id ?? null\n const em = requireTransactionalReplayEntityManager(params.ctx)\n await lockReplayAuthorizationState(em, params.ctx, {\n targetRoleIds: targetRoleId ? [targetRoleId] : [],\n targetTenantIds: [\n ...(before?.tenantId ? [before.tenantId] : []),\n ...(after?.tenantId ? [after.tenantId] : []),\n ...(params.logEntry.tenantId ? [params.logEntry.tenantId] : []),\n ],\n })\n}\n\nregisterCommand(createRoleCommand)\nregisterCommand(updateRoleCommand)\nregisterCommand(deleteRoleCommand)\n\nfunction serializeRole(role: Role, custom?: Record<string, unknown> | null): SerializedRole {\n const payload: SerializedRole = {\n name: String(role.name ?? ''),\n tenantId: String(role.tenantId),\n }\n if (custom && Object.keys(custom).length) payload.custom = custom\n return payload\n}\n\nfunction captureRoleSnapshots(\n role: Role,\n acls: RoleAclSnapshot[] = [],\n custom?: Record<string, unknown> | null\n): RoleSnapshots {\n return {\n view: serializeRole(role, custom),\n undo: {\n id: String(role.id),\n name: String(role.name ?? ''),\n tenantId: String(role.tenantId),\n acls,\n ...(custom && Object.keys(custom).length ? { custom } : {}),\n },\n }\n}\n\nasync function loadRoleAclSnapshots(em: EntityManager, roleId: string): Promise<RoleAclSnapshot[]> {\n const entries = await findWithDecryption(em, RoleAcl, { role: roleId as unknown as Role, deletedAt: null }, {}, { tenantId: null, organizationId: null })\n return entries.map((entry) => ({\n id: entry.id ? String(entry.id) : null,\n tenantId: String(entry.tenantId),\n features: Array.isArray(entry.featuresJson) ? [...entry.featuresJson] : null,\n isSuperAdmin: Boolean(entry.isSuperAdmin),\n organizations: Array.isArray(entry.organizationsJson) ? [...entry.organizationsJson] : null,\n }))\n}\n\nasync function restoreRoleAcls(em: EntityManager, roleId: string, acls: RoleAclSnapshot[]) {\n await lockRoleWriterAuthorizationState(em, [roleId])\n await em.nativeDelete(RoleAcl, { role: roleId as unknown as Role })\n if (!acls.length) {\n await em.flush()\n return\n }\n const roleRef = em.getReference(Role, roleId)\n for (const acl of acls) {\n const entity = em.create(RoleAcl, {\n id: acl.id ?? undefined,\n role: roleRef,\n tenantId: acl.tenantId,\n featuresJson: acl.features ?? null,\n isSuperAdmin: acl.isSuperAdmin,\n organizationsJson: acl.organizations ?? null,\n createdAt: new Date(),\n })\n em.persist(entity)\n }\n await em.flush()\n}\n\ntype RoleUndoPayload = { before?: RoleUndoSnapshot | null; after?: RoleUndoSnapshot | null }\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CommandHandler, CommandReplayAuthorizationArgs, CommandUndoLogEntry } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n parseWithCustomFields,\n setCustomFieldsIfAny,\n emitCrudSideEffects,\n emitCrudUndoSideEffects,\n buildChanges,\n requireId,\n} from '@open-mercato/shared/lib/commands/helpers'\nimport type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport { CrudHttpError, forbidden, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { Role, RoleAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport {\n loadCustomFieldSnapshot,\n buildCustomFieldResetMap,\n diffCustomFieldChanges,\n} from '@open-mercato/shared/lib/commands/customFieldSnapshots'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { resolveIsSuperAdmin, normalizeTenantId } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport {\n assertActorCanAccessRoleTarget,\n assertActorCanGrantAcl,\n assertActorCanModifySuperAdminRoleTarget,\n} from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n assertReplaySnapshotMatches,\n extractStoredReplayInput,\n lockReplayAuthorizationState,\n rerunReplayTransactionGuardAfterLocks,\n requireCurrentReplayFeature,\n requireCurrentReplaySuperAdmin,\n requireTransactionalReplayEntityManager,\n} from '@open-mercato/core/modules/auth/lib/commandReplay'\nimport { lockRoleWriterAuthorizationState } from '@open-mercato/core/modules/auth/lib/authorizationStateLocks'\n\ntype SerializedRole = {\n name: string\n tenantId: string\n custom?: Record<string, unknown>\n}\n\ntype RoleAclSnapshot = {\n id: string | null\n tenantId: string\n features: string[] | null\n isSuperAdmin: boolean\n organizations: string[] | null\n}\n\ntype RoleUndoSnapshot = {\n id: string\n name: string\n tenantId: string\n acls: RoleAclSnapshot[]\n custom?: Record<string, unknown>\n}\n\ntype RoleSnapshots = {\n view: SerializedRole\n undo: RoleUndoSnapshot\n}\n\nconst ROLE_REDO_LOG = Symbol('auth.roles.redoLog')\n\ntype RoleReplayRuntimeContext = CommandRuntimeContext & {\n [ROLE_REDO_LOG]?: CommandUndoLogEntry\n}\n\nfunction withRoleRedoLog(ctx: CommandRuntimeContext, logEntry: CommandUndoLogEntry): RoleReplayRuntimeContext {\n return { ...ctx, [ROLE_REDO_LOG]: logEntry }\n}\n\nfunction resolveCommandEm(ctx: CommandRuntimeContext, forkWhenUnbound = false): EntityManager {\n if (ctx.transactionalEm) return ctx.transactionalEm\n const em = ctx.container.resolve('em') as EntityManager\n return forkWhenUnbound ? em.fork() : em\n}\n\nfunction resolveActorTenantScope(ctx: CommandRuntimeContext): string | null {\n if (ctx.systemActor === true) return null\n const auth = ctx.auth\n if (!auth) return null\n if ((auth as { isSuperAdmin?: boolean }).isSuperAdmin === true) return null\n return normalizeTenantId(auth.tenantId ?? null) ?? null\n}\n\nfunction assertRoleTenantInScope(actorTenantScope: string | null, targetTenantId: unknown): void {\n if (!actorTenantScope) return\n const targetTenant = normalizeTenantId(targetTenantId) ?? null\n if (!targetTenant || targetTenant !== actorTenantScope) {\n throw new CrudHttpError(404, { error: 'Role not found' })\n }\n}\n\nconst RESERVED_ROLE_NAMES = new Set(['superadmin', 'admin'])\n\nfunction isReservedRoleName(name: string | undefined | null): boolean {\n if (typeof name !== 'string') return false\n const normalized = name.trim().toLowerCase()\n return normalized.length > 0 && RESERVED_ROLE_NAMES.has(normalized)\n}\n\nfunction assertRoleNameAllowed(name: string | undefined | null) {\n if (isReservedRoleName(name)) {\n throw new CrudHttpError(400, { error: 'Role name is reserved' })\n }\n}\n\ntype ResolvedActorScope = { isSuperAdmin: boolean; actorTenantId: string | null }\n\nasync function resolveActorScope(ctx: { auth: { tenantId?: string | null; sub?: string | null; orgId?: string | null; isSuperAdmin?: boolean } | null; container: { resolve<T = unknown>(name: string): T } }): Promise<ResolvedActorScope> {\n const isSuperAdmin = await resolveIsSuperAdmin(ctx)\n const actorTenantId = normalizeTenantId(ctx.auth?.tenantId ?? null) ?? null\n return { isSuperAdmin, actorTenantId }\n}\n\nfunction buildScopedRoleFilter(roleId: string, scope: ResolvedActorScope): FilterQuery<Role> {\n const filter: FilterQuery<Role> = { id: roleId, deletedAt: null }\n if (!scope.isSuperAdmin) {\n ;(filter as Record<string, unknown>).tenantId = scope.actorTenantId\n }\n return filter\n}\n\nconst createSchema = z.object({\n name: z.string().min(2).max(100),\n tenantId: z.string().uuid().optional(),\n})\n\nconst updateSchema = z.object({\n id: z.string().uuid(),\n name: z.string().min(2).max(100).optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const roleCrudEvents: CrudEventsConfig = {\n module: 'auth',\n entity: 'role',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\nexport const roleCrudIndexer: CrudIndexerConfig = {\n entityType: E.auth.role,\n buildUpsertPayload: (ctx) => ({\n entityType: E.auth.role,\n recordId: ctx.identifiers.id,\n tenantId: ctx.identifiers.tenantId,\n }),\n buildDeletePayload: (ctx) => ({\n entityType: E.auth.role,\n recordId: ctx.identifiers.id,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\nconst createRoleCommand: CommandHandler<Record<string, unknown>, Role> = {\n id: 'auth.roles.create',\n atomicReplay: true,\n stabilizeReplay: stabilizeRoleReplay,\n authorizeReplay: (params) => authorizeRoleReplay('create', params),\n async execute(rawInput, ctx) {\n const rawBody = rawInput && typeof rawInput === 'object' ? rawInput as Record<string, unknown> : {}\n if ('tenantId' in rawBody && rawBody.tenantId === null) {\n throw new CrudHttpError(400, { error: 'tenantId cannot be null \u2014 global roles are not supported' })\n }\n const { parsed, custom } = parseWithCustomFields(createSchema, rawInput)\n assertRoleNameAllowed(parsed.name)\n const scope = await resolveActorScope(ctx)\n const requestedTenantId = parsed.tenantId ?? null\n if (!scope.isSuperAdmin && requestedTenantId && requestedTenantId !== scope.actorTenantId) {\n throw new CrudHttpError(403, { error: 'Not authorized to target this tenant.' })\n }\n const resolvedTenantId = scope.isSuperAdmin\n ? (requestedTenantId ?? scope.actorTenantId)\n : scope.actorTenantId\n if (!resolvedTenantId) {\n throw new CrudHttpError(400, { error: 'tenantId is required \u2014 global roles are not supported' })\n }\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n let role: Role\n try {\n role = await de.createOrmEntity({\n entity: Role,\n data: {\n name: parsed.name,\n tenantId: resolvedTenantId,\n },\n })\n } catch (error) {\n if (!isUniqueViolation(error, 'roles_tenant_id_name_unique')) throw error\n const { translate } = await resolveTranslations()\n const message = translate('auth.roles.errors.nameExists', 'A role with this name already exists')\n throw new CrudHttpError(400, {\n error: message,\n fieldErrors: { name: message },\n details: [{ path: ['name'], message, code: 'duplicate', origin: 'validation' }],\n })\n }\n\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: String(role.id),\n organizationId: null,\n tenantId: resolvedTenantId,\n values: custom,\n })\n\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: role,\n identifiers: {\n id: String(role.id),\n organizationId: null,\n tenantId: resolvedTenantId,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n\n return role\n },\n captureAfter: async (_input, result, ctx) => {\n const em = resolveCommandEm(ctx, true)\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n })\n return serializeRole(result, custom)\n },\n buildLog: async ({ result, ctx }) => {\n const { translate } = await resolveTranslations()\n const em = resolveCommandEm(ctx, true)\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n })\n const snapshot = captureRoleSnapshots(result, [], custom)\n return {\n actionLabel: translate('auth.audit.roles.create', 'Create role'),\n resourceKind: 'auth.role',\n resourceId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n snapshotAfter: snapshot.view,\n payload: {\n undo: {\n after: snapshot.undo,\n },\n },\n }\n },\n undo: async ({ input, logEntry, ctx }) => {\n const undo = extractUndoPayload<RoleUndoPayload>(logEntry)?.after\n if (!undo) return\n const em = resolveCommandEm(ctx)\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await withAtomicFlush(em, [async () => {\n await authorizeRoleReplayAtMutation('create', {\n operation: 'undo',\n input,\n ctx,\n logEntry,\n })\n await lockRoleWriterAuthorizationState(em, [undo.id])\n await em.nativeDelete(RoleAcl, { role: undo.id as unknown as Role })\n if (undo.custom && Object.keys(undo.custom).length) {\n const reset = buildCustomFieldResetMap(undefined, undo.custom)\n if (Object.keys(reset).length) {\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: undo.id,\n organizationId: null,\n tenantId: undo.tenantId ?? null,\n values: reset,\n notify: false,\n })\n }\n }\n await de.deleteOrmEntity({\n entity: Role,\n where: { id: undo.id, deletedAt: null } as FilterQuery<Role>,\n soft: false,\n })\n }], { transaction: true, label: 'auth.roles.create.undo' })\n },\n redo: async ({ input, logEntry, ctx }) => {\n const after = resolveRedoSnapshot<RoleUndoSnapshot>(logEntry)\n if (!after) throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for role create' })\n const em = resolveCommandEm(ctx)\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n let role: Role | null = null\n await withAtomicFlush(em, [async () => {\n await authorizeRoleReplayAtMutation('create', {\n operation: 'redo',\n input,\n ctx,\n logEntry,\n })\n role = await findOneWithDecryption(em, Role, { id: after.id }, {}, { tenantId: null, organizationId: null })\n if (role) {\n role.deletedAt = null\n role.name = after.name\n role.tenantId = after.tenantId\n await em.flush()\n } else {\n role = await de.createOrmEntity({\n entity: Role,\n data: {\n id: after.id,\n name: after.name,\n tenantId: after.tenantId,\n },\n })\n }\n await restoreRoleAcls(em, after.id, after.acls)\n if (after.custom && Object.keys(after.custom).length) {\n const reset = buildCustomFieldResetMap(after.custom, undefined)\n if (Object.keys(reset).length) {\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: after.id,\n organizationId: null,\n tenantId: after.tenantId ?? null,\n values: reset,\n notify: false,\n })\n }\n }\n }], { transaction: true, label: 'auth.roles.create.redo' })\n if (!role) throw new CrudHttpError(400, { error: '[internal] redo failed to restore role row' })\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: role,\n identifiers: {\n id: after.id,\n organizationId: null,\n tenantId: after.tenantId ?? null,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n return role\n },\n}\n\nconst updateRoleCommand: CommandHandler<Record<string, unknown>, Role> = {\n id: 'auth.roles.update',\n atomicReplay: true,\n stabilizeReplay: stabilizeRoleReplay,\n authorizeReplay: (params) => authorizeRoleReplay('update', params),\n async prepare(rawInput, ctx) {\n const { parsed } = parseWithCustomFields(updateSchema, rawInput)\n const em = resolveCommandEm(ctx)\n const scope = await resolveActorScope(ctx)\n const existing = await findOneWithDecryption(em, Role, buildScopedRoleFilter(parsed.id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null })\n if (!existing) throw new CrudHttpError(404, { error: 'Role not found' })\n assertRoleTenantInScope(resolveActorTenantScope(ctx), existing.tenantId)\n const acls = await loadRoleAclSnapshots(em, parsed.id)\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: parsed.id,\n tenantId: existing.tenantId ? String(existing.tenantId) : null,\n })\n return { before: captureRoleSnapshots(existing, acls, custom) }\n },\n async execute(rawInput, ctx) {\n const { parsed, custom } = parseWithCustomFields(updateSchema, rawInput)\n const em = resolveCommandEm(ctx)\n const scope = await resolveActorScope(ctx)\n const current = await findOneWithDecryption(em, Role, buildScopedRoleFilter(parsed.id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null })\n if (!current) throw new CrudHttpError(404, { error: 'Role not found' })\n const actorTenantScope = resolveActorTenantScope(ctx)\n assertRoleTenantInScope(actorTenantScope, current.tenantId)\n if (parsed.name !== undefined) {\n const nextName = parsed.name\n if (nextName !== current.name) assertRoleNameAllowed(nextName)\n if (nextName !== current.name) {\n const assignments = await em.count(UserRole, { role: current, deletedAt: null })\n if (assignments > 0) {\n throw new CrudHttpError(400, { error: 'Role name cannot be changed while users are assigned' })\n }\n }\n }\n const wantsTenantChange = parsed.tenantId !== undefined && parsed.tenantId !== current.tenantId\n if (wantsTenantChange) {\n if (!scope.isSuperAdmin) {\n throw new CrudHttpError(403, { error: 'Not authorized to target this tenant.' })\n }\n const assignments = await em.count(UserRole, { role: current, deletedAt: null })\n if (assignments > 0) {\n throw new CrudHttpError(400, { error: 'Role cannot be moved to another tenant while users are assigned' })\n }\n }\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n const redoLogEntry = (ctx as RoleReplayRuntimeContext)[ROLE_REDO_LOG]\n let role: Role | null = null\n await withAtomicFlush(em, [async () => {\n if (redoLogEntry) {\n await authorizeRoleReplayAtMutation('update', {\n operation: 'redo',\n input: rawInput,\n ctx,\n logEntry: redoLogEntry,\n })\n }\n await lockRoleWriterAuthorizationState(em, [parsed.id])\n if (wantsTenantChange) {\n await em.nativeDelete(RoleAcl, { role: parsed.id as unknown as Role })\n }\n role = await de.updateOrmEntity({\n entity: Role,\n where: buildScopedRoleFilter(parsed.id, scope),\n apply: (entity) => {\n if (parsed.name !== undefined) entity.name = parsed.name\n if (parsed.tenantId !== undefined && scope.isSuperAdmin) entity.tenantId = parsed.tenantId\n },\n })\n if (!role) throw new CrudHttpError(404, { error: 'Role not found' })\n\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: String(role.id),\n organizationId: null,\n tenantId: role.tenantId ? String(role.tenantId) : null,\n values: custom,\n })\n }], {\n transaction: true,\n label: redoLogEntry ? 'auth.roles.update.redo' : 'auth.roles.update',\n })\n const updatedRole = role as Role | null\n if (!updatedRole) throw new CrudHttpError(404, { error: 'Role not found' })\n\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: updatedRole,\n identifiers: {\n id: String(updatedRole.id),\n organizationId: null,\n tenantId: updatedRole.tenantId ? String(updatedRole.tenantId) : null,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n\n return updatedRole\n },\n captureAfter: async (_input, result, ctx) => {\n const em = resolveCommandEm(ctx, true)\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n })\n return serializeRole(result, custom)\n },\n buildLog: async ({ result, snapshots, ctx }) => {\n const { translate } = await resolveTranslations()\n const beforeSnapshots = snapshots.before as RoleSnapshots | undefined\n const before = beforeSnapshots?.view\n const beforeUndo = beforeSnapshots?.undo ?? null\n const em = resolveCommandEm(ctx, true)\n const afterAcls = await loadRoleAclSnapshots(em, String(result.id))\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n })\n const afterSnapshots = captureRoleSnapshots(result, afterAcls, custom)\n const after = afterSnapshots.view\n const changes = buildChanges(before ?? null, after as Record<string, unknown>, ['name', 'tenantId'])\n const customDiff = diffCustomFieldChanges(before?.custom, custom)\n for (const [key, diff] of Object.entries(customDiff)) {\n changes[`cf_${key}`] = diff\n }\n return {\n actionLabel: translate('auth.audit.roles.update', 'Update role'),\n resourceKind: 'auth.role',\n resourceId: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n changes,\n snapshotBefore: before ?? null,\n snapshotAfter: after,\n payload: {\n undo: {\n before: beforeUndo,\n after: afterSnapshots.undo,\n },\n },\n }\n },\n undo: async ({ input, logEntry, ctx }) => {\n const undo = extractUndoPayload<RoleUndoPayload>(logEntry)\n const before = undo?.before\n const after = undo?.after\n if (!before) return\n const storedReplayInput = extractStoredReplayInput(logEntry.commandPayload, logEntry)\n const replayInput = Object.keys(storedReplayInput).length\n ? storedReplayInput\n : deriveRoleUpdateReplayInput(before, after ?? null)\n const em = resolveCommandEm(ctx)\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n let updated: Role | null = null\n await withAtomicFlush(em, [async () => {\n await authorizeRoleReplayAtMutation('update', {\n operation: 'undo',\n input,\n ctx,\n logEntry,\n })\n updated = await de.updateOrmEntity({\n entity: Role,\n where: { id: before.id, deletedAt: null } as FilterQuery<Role>,\n apply: (entity) => {\n if ('name' in replayInput) entity.name = before.name\n if ('tenantId' in replayInput) entity.tenantId = before.tenantId\n },\n })\n const restoresAcls = 'tenantId' in replayInput && after?.tenantId !== before.tenantId\n if (updated && restoresAcls) {\n await restoreRoleAcls(em, before.id, before.acls)\n }\n const reset = buildCustomFieldResetMap(before.custom, after?.custom)\n if (Object.keys(reset).length) {\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: before.id,\n organizationId: null,\n tenantId: before.tenantId,\n values: reset,\n notify: false,\n })\n }\n }], { transaction: true, label: 'auth.roles.update.undo' })\n await emitCrudUndoSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: updated,\n identifiers: {\n id: before.id,\n organizationId: null,\n tenantId: before.tenantId ?? null,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n },\n redo: ({ input, ctx, logEntry }) => updateRoleCommand.execute(input, withRoleRedoLog(ctx, logEntry)),\n}\n\nconst deleteRoleCommand: CommandHandler<{ body?: Record<string, unknown>; query?: Record<string, unknown> }, Role> = {\n id: 'auth.roles.delete',\n atomicReplay: true,\n stabilizeReplay: stabilizeRoleReplay,\n authorizeReplay: (params) => authorizeRoleReplay('delete', params),\n async prepare(input, ctx) {\n const id = requireId(input, 'Role id required')\n const em = resolveCommandEm(ctx)\n const scope = await resolveActorScope(ctx)\n const existing = await findOneWithDecryption(em, Role, buildScopedRoleFilter(id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null })\n if (!existing) return {}\n const actorTenantScope = resolveActorTenantScope(ctx)\n if (actorTenantScope) {\n const targetTenant = normalizeTenantId(existing.tenantId) ?? null\n if (!targetTenant || targetTenant !== actorTenantScope) return {}\n }\n const acls = await loadRoleAclSnapshots(em, id)\n const custom = await loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: id,\n tenantId: existing.tenantId ? String(existing.tenantId) : null,\n })\n return { before: captureRoleSnapshots(existing, acls, custom) }\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Role id required')\n const em = resolveCommandEm(ctx)\n const scope = await resolveActorScope(ctx)\n const role = await findOneWithDecryption(em, Role, buildScopedRoleFilter(id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null })\n if (!role) throw new CrudHttpError(404, { error: 'Role not found' })\n assertRoleTenantInScope(resolveActorTenantScope(ctx), role.tenantId)\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n const redoLogEntry = (ctx as RoleReplayRuntimeContext)[ROLE_REDO_LOG]\n let deleted: Role | null = null\n await withAtomicFlush(em, [async () => {\n if (redoLogEntry) {\n await authorizeRoleReplayAtMutation('delete', {\n operation: 'redo',\n input,\n ctx,\n logEntry: redoLogEntry,\n })\n }\n await lockRoleWriterAuthorizationState(em, [id])\n const activeAssignments = await em.count(UserRole, { role, deletedAt: null })\n if (activeAssignments > 0) throw new CrudHttpError(400, { error: 'Role has assigned users' })\n await em.nativeDelete(RoleAcl, { role: id })\n deleted = await de.deleteOrmEntity({\n entity: Role,\n where: buildScopedRoleFilter(id, scope),\n soft: false,\n })\n if (!deleted) throw new CrudHttpError(404, { error: 'Role not found' })\n }], {\n transaction: true,\n label: redoLogEntry ? 'auth.roles.delete.redo' : 'auth.roles.delete',\n })\n const deletedRole = deleted as Role | null\n if (!deletedRole) throw new CrudHttpError(404, { error: 'Role not found' })\n\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'deleted',\n entity: deletedRole,\n identifiers: {\n id,\n organizationId: null,\n tenantId: deletedRole.tenantId ? String(deletedRole.tenantId) : null,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n\n return deletedRole\n },\n buildLog: async ({ snapshots, input }) => {\n const { translate } = await resolveTranslations()\n const beforeSnapshots = snapshots.before as RoleSnapshots | undefined\n const before = beforeSnapshots?.view\n const beforeUndo = beforeSnapshots?.undo ?? null\n const id = requireId(input, 'Role id required')\n return {\n actionLabel: translate('auth.audit.roles.delete', 'Delete role'),\n resourceKind: 'auth.role',\n resourceId: id,\n tenantId: before?.tenantId ?? null,\n snapshotBefore: before ?? null,\n payload: {\n undo: {\n before: beforeUndo,\n },\n },\n }\n },\n undo: async ({ input, logEntry, ctx }) => {\n const before = extractUndoPayload<RoleUndoPayload>(logEntry)?.before\n if (!before) return\n const em = resolveCommandEm(ctx)\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n let role: Role | null = null\n await withAtomicFlush(em, [async () => {\n await authorizeRoleReplayAtMutation('delete', {\n operation: 'undo',\n input,\n ctx,\n logEntry,\n })\n role = await findOneWithDecryption(em, Role, { id: before.id }, {}, { tenantId: null, organizationId: null })\n if (role) {\n role.deletedAt = null\n role.name = before.name\n role.tenantId = before.tenantId\n await em.flush()\n } else {\n role = await de.createOrmEntity({\n entity: Role,\n data: {\n id: before.id,\n name: before.name,\n tenantId: before.tenantId,\n },\n })\n }\n await restoreRoleAcls(em, before.id, before.acls)\n const reset = buildCustomFieldResetMap(before.custom, undefined)\n if (Object.keys(reset).length) {\n await setCustomFieldsIfAny({\n dataEngine: de,\n entityId: E.auth.role,\n recordId: before.id,\n organizationId: null,\n tenantId: before.tenantId ?? null,\n values: reset,\n notify: false,\n })\n }\n }], { transaction: true, label: 'auth.roles.delete.undo' })\n if (!role) throw new CrudHttpError(400, { error: '[internal] undo failed to restore role row' })\n await emitCrudUndoSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: role,\n identifiers: {\n id: before.id,\n organizationId: null,\n tenantId: before.tenantId ?? null,\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n })\n },\n redo: ({ input, ctx, logEntry }) => deleteRoleCommand.execute(input, withRoleRedoLog(ctx, logEntry)),\n}\n\ntype RoleReplayCommandKind = 'create' | 'update' | 'delete'\n\nfunction normalizeRoleAclSnapshots(acls: RoleAclSnapshot[]): RoleAclSnapshot[] {\n return acls\n .map((acl) => ({\n ...acl,\n features: acl.features ? [...acl.features].sort((left, right) => left.localeCompare(right)) : null,\n organizations: acl.organizations ? [...acl.organizations].sort((left, right) => left.localeCompare(right)) : null,\n }))\n .sort((left, right) => {\n const tenantOrder = left.tenantId.localeCompare(right.tenantId)\n if (tenantOrder !== 0) return tenantOrder\n return (left.id ?? '').localeCompare(right.id ?? '')\n })\n}\n\nfunction normalizeRoleReplaySnapshot(snapshot: RoleUndoSnapshot): RoleUndoSnapshot {\n return {\n ...snapshot,\n acls: normalizeRoleAclSnapshots(snapshot.acls),\n }\n}\n\nfunction toRoleUpdateReplayState(snapshot: RoleUndoSnapshot): SerializedRole {\n return {\n name: snapshot.name,\n tenantId: snapshot.tenantId,\n ...(snapshot.custom && Object.keys(snapshot.custom).length ? { custom: snapshot.custom } : {}),\n }\n}\n\nfunction deriveRoleUpdateReplayInput(\n before: RoleUndoSnapshot | null,\n after: RoleUndoSnapshot | null,\n): Record<string, unknown> {\n if (!before) return {}\n if (!after) return { id: before.id, name: before.name, tenantId: before.tenantId }\n const input: Record<string, unknown> = { id: after.id }\n if (before.name !== after.name) input.name = after.name\n if (before.tenantId !== after.tenantId) input.tenantId = after.tenantId\n return input\n}\n\nasync function authorizeRoleReplay(\n commandKind: RoleReplayCommandKind,\n params: CommandReplayAuthorizationArgs<unknown>,\n): Promise<void> {\n const { operation, ctx, logEntry } = params\n const em = requireTransactionalReplayEntityManager(ctx)\n await requireCurrentReplayFeature(ctx, 'auth.roles.manage')\n\n const undoPayload = extractUndoPayload<RoleUndoPayload>(logEntry)\n const before = undoPayload?.before ?? null\n const after = undoPayload?.after ?? null\n const roleId = logEntry.resourceId ?? before?.id ?? after?.id ?? null\n if (!roleId) {\n await assertReplaySnapshotMatches(null, { required: 'role replay snapshot' })\n return\n }\n\n const currentRole = await findOneWithDecryption(\n em,\n Role,\n { id: roleId, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n const rbacService = ctx.systemActor === true\n ? null\n : ctx.container.resolve('rbacService') as RbacService\n if (rbacService && currentRole) {\n await assertActorCanAccessRoleTarget({\n em,\n rbacService,\n requireBoundRbac: true,\n actorUserId: ctx.auth?.sub,\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.auth?.orgId ?? null,\n targetRoleId: roleId,\n })\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService,\n requireBoundRbac: true,\n actorUserId: ctx.auth?.sub,\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.auth?.orgId ?? null,\n targetRoleId: roleId,\n })\n }\n\n let currentSnapshot: RoleUndoSnapshot | null = null\n if (currentRole) {\n const currentTenantId = currentRole.tenantId ? String(currentRole.tenantId) : null\n const [acls, custom] = await Promise.all([\n loadRoleAclSnapshots(em, roleId),\n loadCustomFieldSnapshot(em, {\n entityId: E.auth.role,\n recordId: roleId,\n tenantId: currentTenantId,\n }),\n ])\n currentSnapshot = captureRoleSnapshots(currentRole, acls, custom).undo\n }\n\n const expectedCurrent = operation === 'undo'\n ? commandKind === 'create' ? after : commandKind === 'update' ? after : null\n : commandKind === 'create' ? null : before\n const storedReplayInput = extractStoredReplayInput(params.input, logEntry)\n const replayInput = Object.keys(storedReplayInput).length\n ? storedReplayInput\n : deriveRoleUpdateReplayInput(before, after)\n const movesTenant = commandKind === 'update'\n && 'tenantId' in replayInput\n && before?.tenantId !== after?.tenantId\n if (movesTenant) {\n await requireCurrentReplaySuperAdmin(ctx)\n }\n\n if (commandKind === 'update' && !movesTenant) {\n if (!currentSnapshot || !expectedCurrent) {\n await assertReplaySnapshotMatches(currentSnapshot, expectedCurrent)\n } else {\n await assertReplaySnapshotMatches(\n toRoleUpdateReplayState(currentSnapshot),\n toRoleUpdateReplayState(expectedCurrent),\n )\n }\n } else {\n await assertReplaySnapshotMatches(\n currentSnapshot ? normalizeRoleReplaySnapshot(currentSnapshot) : null,\n expectedCurrent ? normalizeRoleReplaySnapshot(expectedCurrent) : null,\n )\n }\n\n const desiredState = operation === 'undo'\n ? commandKind === 'create' ? null : before\n : commandKind === 'delete' ? null : after\n if (desiredState && !currentRole && rbacService) {\n const actorUserId = ctx.auth?.sub\n if (!actorUserId) throw forbidden()\n const actorAcl = await rbacService.loadAclWithEntityManager(em, actorUserId, {\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.auth?.orgId ?? null,\n })\n if (!actorAcl.isSuperAdmin) assertRoleTenantInScope(ctx.auth?.tenantId ?? null, desiredState.tenantId)\n }\n const restoresAcls = Boolean(\n desiredState\n && desiredState.acls.length > 0\n && (commandKind !== 'update' || movesTenant),\n )\n if (!restoresAcls || !desiredState || !rbacService) return\n\n await requireCurrentReplayFeature(ctx, 'auth.acl.manage')\n for (const acl of desiredState.acls) {\n await assertActorCanGrantAcl({\n em,\n rbacService,\n requireBoundRbac: true,\n actorUserId: ctx.auth?.sub,\n tenantId: acl.tenantId,\n organizationId: null,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n organizations: acl.organizations,\n })\n }\n}\n\nasync function authorizeRoleReplayAtMutation(\n commandKind: RoleReplayCommandKind,\n params: CommandReplayAuthorizationArgs<unknown>,\n): Promise<void> {\n const em = params.ctx.transactionalEm\n ?? params.ctx.container.resolve('em') as EntityManager\n const boundParams = params.ctx.transactionalEm\n ? params\n : { ...params, ctx: { ...params.ctx, transactionalEm: em } }\n await stabilizeRoleReplay(boundParams)\n await rerunReplayTransactionGuardAfterLocks(boundParams, em)\n await authorizeRoleReplay(commandKind, boundParams)\n}\n\nasync function stabilizeRoleReplay(\n params: CommandReplayAuthorizationArgs<unknown>,\n): Promise<void> {\n const undoPayload = extractUndoPayload<RoleUndoPayload>(params.logEntry)\n const before = undoPayload?.before ?? null\n const after = undoPayload?.after ?? null\n const targetRoleId = params.logEntry.resourceId ?? before?.id ?? after?.id ?? null\n const em = requireTransactionalReplayEntityManager(params.ctx)\n await lockReplayAuthorizationState(em, params.ctx, {\n targetRoleIds: targetRoleId ? [targetRoleId] : [],\n targetTenantIds: [\n ...(before?.tenantId ? [before.tenantId] : []),\n ...(after?.tenantId ? [after.tenantId] : []),\n ...(params.logEntry.tenantId ? [params.logEntry.tenantId] : []),\n ],\n })\n}\n\nregisterCommand(createRoleCommand)\nregisterCommand(updateRoleCommand)\nregisterCommand(deleteRoleCommand)\n\nfunction serializeRole(role: Role, custom?: Record<string, unknown> | null): SerializedRole {\n const payload: SerializedRole = {\n name: String(role.name ?? ''),\n tenantId: String(role.tenantId),\n }\n if (custom && Object.keys(custom).length) payload.custom = custom\n return payload\n}\n\nfunction captureRoleSnapshots(\n role: Role,\n acls: RoleAclSnapshot[] = [],\n custom?: Record<string, unknown> | null\n): RoleSnapshots {\n return {\n view: serializeRole(role, custom),\n undo: {\n id: String(role.id),\n name: String(role.name ?? ''),\n tenantId: String(role.tenantId),\n acls,\n ...(custom && Object.keys(custom).length ? { custom } : {}),\n },\n }\n}\n\nasync function loadRoleAclSnapshots(em: EntityManager, roleId: string): Promise<RoleAclSnapshot[]> {\n const entries = await findWithDecryption(em, RoleAcl, { role: roleId as unknown as Role, deletedAt: null }, {}, { tenantId: null, organizationId: null })\n return entries.map((entry) => ({\n id: entry.id ? String(entry.id) : null,\n tenantId: String(entry.tenantId),\n features: Array.isArray(entry.featuresJson) ? [...entry.featuresJson] : null,\n isSuperAdmin: Boolean(entry.isSuperAdmin),\n organizations: Array.isArray(entry.organizationsJson) ? [...entry.organizationsJson] : null,\n }))\n}\n\nasync function restoreRoleAcls(em: EntityManager, roleId: string, acls: RoleAclSnapshot[]) {\n await lockRoleWriterAuthorizationState(em, [roleId])\n await em.nativeDelete(RoleAcl, { role: roleId as unknown as Role })\n if (!acls.length) {\n await em.flush()\n return\n }\n const roleRef = em.getReference(Role, roleId)\n for (const acl of acls) {\n const entity = em.create(RoleAcl, {\n id: acl.id ?? undefined,\n role: roleRef,\n tenantId: acl.tenantId,\n featuresJson: acl.features ?? null,\n isSuperAdmin: acl.isSuperAdmin,\n organizationsJson: acl.organizations ?? null,\n createdAt: new Date(),\n })\n em.persist(entity)\n }\n await em.flush()\n}\n\ntype RoleUndoPayload = { before?: RoleUndoSnapshot | null; after?: RoleUndoSnapshot | null }\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -102,9 +102,9 @@ async function setRecordCustomFields(em, opts) {
102
102
  const def = defsByKey?.get(fieldKey);
103
103
  const encrypted = Boolean(def?.configJson && def.configJson?.encrypted);
104
104
  const collapseToSingle = Array.isArray(raw) && Boolean(def) && def.configJson?.multi !== true;
105
- const isArray = Array.isArray(raw) && !collapseToSingle;
105
+ const isArray = Array.isArray(raw) && !collapseToSingle || raw === null && def?.configJson?.multi === true;
106
106
  if (isArray) {
107
- const arr = raw;
107
+ const arr = Array.isArray(raw) ? raw.filter((value2) => value2 != null) : [];
108
108
  await em.nativeDelete(CustomFieldValue, { entityId, recordId, fieldKey, ...reconcileScopeFilter });
109
109
  for (const val of arr) {
110
110
  const col = encrypted ? "valueText" : def ? columnFromKind(def.kind) : columnFromJsValue(val);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/lib/helpers.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { decryptCustomFieldValue, encryptCustomFieldValue, resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport {\n MAX_CUSTOM_FIELD_KEYS_PER_RECORD,\n TOO_MANY_CUSTOM_FIELDS_ERROR,\n} from '@open-mercato/shared/modules/entities/validation'\nimport { CustomFieldDef, CustomFieldValue } from '../data/entities'\nimport { loadScopedCustomFieldDefs } from './scoped-field-defs'\nimport { encryptCustomEntityDocument } from '@open-mercato/shared/lib/encryption/customEntityDoc'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\n\n/** @internal */\nexport async function encryptCustomEntityRecordDocument(\n em: EntityManager,\n opts: { entityId: string; organizationId: string | null; tenantId: string | null; doc: Record<string, unknown>; encryptionService?: TenantDataEncryptionService | null },\n): Promise<Record<string, unknown>> {\n if (!isTenantDataEncryptionEnabled() || Object.keys(opts.doc).every((key) => key === 'id')) return opts.doc\n const definitions = await loadScopedCustomFieldDefs(em, opts)\n const fields = Array.from(definitions.values(), (definition) => ({\n key: definition.key,\n kind: definition.kind,\n encrypted: Boolean(definition.configJson?.encrypted),\n }))\n return encryptCustomEntityDocument(opts.doc, fields, opts.tenantId, resolveTenantEncryptionService(em, opts.encryptionService))\n}\n\ntype Primitive = string | number | boolean | null | undefined\ntype PrimitiveOrArray = Primitive | Primitive[]\n\nexport type SetRecordCustomFieldsOptions = {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, PrimitiveOrArray>\n // When true (default), try to use field definitions to decide storage column\n preferDefs?: boolean\n // Optional: notify external systems (e.g., indexing) when values changed\n onChanged?: (payload: { entityId: string; recordId: string; organizationId: string | null; tenantId: string | null }) => Promise<void> | void\n // Optional: re-use an existing tenant encryption service instance\n encryptionService?: TenantDataEncryptionService | null\n // When true, additionally scope the reconciling delete/lookup by organizationId,\n // restoring the pre-#5970 organization pin. Default false relies on `recordId`\n // being unique across organizations for a given entityId \u2014 true for every\n // ORM-backed entity, where recordId is the record's own primary key. That\n // invariant does not hold for `custom_entities_storage` records written under\n // `ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM`: the table upserts on\n // `(entity_type, entity_id, organization_id)`, so the same id can be a live\n // record in two organizations of one tenant, and an organization-agnostic\n // delete could remove another organization's values (#6034 review). Callers\n // writing those records must pass pinOrganizationId: true.\n pinOrganizationId?: boolean\n}\n\nfunction columnFromKind(kind: string): keyof CustomFieldValue {\n switch (kind) {\n case 'text':\n case 'select':\n case 'currency':\n case 'dictionary':\n case 'phone':\n return 'valueText'\n case 'multiline':\n return 'valueMultiline'\n case 'integer':\n return 'valueInt'\n case 'float':\n return 'valueFloat'\n case 'boolean':\n return 'valueBool'\n default:\n return 'valueText'\n }\n}\n\nfunction columnFromJsValue(v: Primitive): keyof CustomFieldValue {\n if (v === null || v === undefined) return 'valueText'\n if (typeof v === 'boolean') return 'valueBool'\n if (typeof v === 'number') return Number.isInteger(v) ? 'valueInt' : 'valueFloat'\n return 'valueText'\n}\n\n// Clears all value columns to avoid leftovers on update\nfunction clearValueColumns(cf: CustomFieldValue) {\n cf.valueText = null\n cf.valueMultiline = null\n cf.valueInt = null\n cf.valueFloat = null\n cf.valueBool = null\n}\n\nfunction valueFromRow(row: CustomFieldValue): Primitive {\n if (row.valueMultiline != null) return row.valueMultiline\n if (row.valueText != null) return row.valueText\n if (row.valueInt != null) return row.valueInt\n if (row.valueFloat != null) return row.valueFloat\n if (row.valueBool != null) return row.valueBool\n return null\n}\n\n// Keeps the value the caller's own scope already holds when the echoed array contains it,\n// so a save that never touched the field does not swap in another organization's value.\nfunction pickSingleValue(candidates: Primitive[], currentValue: unknown): Primitive {\n if (currentValue != null) {\n const kept = candidates.find((candidate) => candidate != null && String(candidate) === String(currentValue))\n if (kept !== undefined) return kept\n }\n const present = candidates.filter((candidate) => candidate != null)\n return present.length ? present[present.length - 1] : null\n}\n\nexport async function setRecordCustomFields(\n em: EntityManager,\n opts: SetRecordCustomFieldsOptions,\n): Promise<void> {\n const { entityId, recordId, values } = opts\n const organizationId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const preferDefs = opts.preferDefs !== false\n\n let defsByKey: Map<string, CustomFieldDef> | undefined\n if (preferDefs) {\n defsByKey = await loadScopedCustomFieldDefs(em, { entityId, organizationId, tenantId })\n }\n\n const toPersist: CustomFieldValue[] = []\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 as any, opts.encryptionService)\n return encryptionService\n }\n // A write owns every value row a reader in ITS OWN tenant scope could return for this\n // record: same logical key (entityId, recordId, fieldKey), any organization, and either\n // the caller's tenant or the instance-global NULL tenant \u2014 exactly what the Query Engine\n // matches. Organization is left out because the Query Engine's value join does not filter\n // it (engine.ts, the cf join and the EXISTS subquery), so a row from a scope the record\n // has left is indistinguishable from the live one (#5970). The query-index reader does\n // scope organization_id, but it never sees another organization's row for a record it is\n // indexing, so dropping the column here cannot widen what it returns.\n //\n // This relies on recordId being unique across organizations and tenants for a given\n // entityId \u2014 true of every ORM-backed caller, which passes the record's own primary key.\n // A caller that passed a per-organization natural key \u2014 or a recordId that is only unique\n // WITHIN an organization, as custom_entities_storage's EAV backcompat writes are \u2014 would\n // have one organization's write delete another's live rows. Those callers opt into\n // pinOrganizationId (see SetRecordCustomFieldsOptions) to restore the organization pin.\n //\n // Scope of the delete, precisely: another TENANT's own rows are never touched. Rows with a\n // NULL tenant are, deliberately \u2014 they are instance-global and answer every tenant's reads,\n // so leaving one behind would reproduce the very duplicate this reconciles. Spelled as an\n // explicit $or rather than `$in: [tenantId, null]` because SQL `IN (NULL)` never matches a\n // NULL row.\n const tenantScopeFilter = { $or: [{ tenantId }, { tenantId: null }] }\n const pinOrganizationId = opts.pinOrganizationId === true\n const reconcileScopeFilter = pinOrganizationId\n ? { organizationId, ...tenantScopeFilter }\n : tenantScopeFilter\n const keys = Object.keys(values)\n const presentKeyCount = keys.filter((key) => values[key] !== undefined).length\n if (preferDefs && presentKeyCount > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {\n throw new Error(TOO_MANY_CUSTOM_FIELDS_ERROR)\n }\n\n // Run the per-key delete+insert work inside ONE database transaction so a\n // multi-value replacement is atomic and isolated. The array branch deletes the\n // existing rows for a key and inserts the replacements; without an enclosing\n // transaction those can land in separate commit boundaries under MikroORM's\n // FlushMode.AUTO (a query elsewhere in the unit auto-flushes part of the work),\n // which intermittently left the field with the delete applied but the inserts\n // missing \u2014 the multi-select EDIT reverted to []. The single commit below makes\n // it all-or-nothing. We only open our own transaction when the caller has not\n // already started one (commands fork the request em and may run setCustomFields\n // outside their own withAtomicFlush tx); join an ambient transaction otherwise.\n const txEm = em as {\n begin?: () => Promise<void>\n commit?: () => Promise<void>\n rollback?: () => Promise<void>\n isInTransaction?: () => boolean\n }\n const txCapable =\n typeof txEm.begin === 'function' &&\n typeof txEm.commit === 'function' &&\n typeof txEm.rollback === 'function' &&\n typeof txEm.isInTransaction === 'function'\n const ownCustomFieldTransaction = txCapable && !txEm.isInTransaction!()\n if (ownCustomFieldTransaction) await txEm.begin!()\n try {\n for (const fieldKey of keys) {\n const raw = values[fieldKey]\n if (raw === undefined) continue\n\n const def = defsByKey?.get(fieldKey)\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n // A single-value definition never stores an array. One arrives when the detail read\n // returned the duplicate rows #5970 left behind as an array and a form echoed it back\n // unchanged; treating it as a multi-value replacement re-creates every duplicate under\n // the caller's organization (#6468). Collapse it to one value so the scalar branch\n // below reconciles the duplicates away.\n const collapseToSingle = Array.isArray(raw) && Boolean(def) && (def as any).configJson?.multi !== true\n const isArray = Array.isArray(raw) && !collapseToSingle\n // When array (multi-value): replace all existing rows for the key. Delete\n // first, then create replacements, all inside the transaction opened above.\n // Creating rows before a native delete can auto-flush and delete the new\n // values; mixing em.remove(stale) with new rows for the same EAV scope was\n // observed to commit an empty set under MikroORM v7. The explicit order keeps\n // the replacement atomic without letting old-row cleanup target new rows.\n if (isArray) {\n const arr = raw as Primitive[]\n // Reconciling delete (see tenantScopeFilter/reconcileScopeFilter): pinning\n // organizationId here left the rows written under the record's PREVIOUS\n // organization alive next to the replacements, and the Query Engine's value join\n // does not filter organization_id, so both generations answered the same lookup\n // (#5970) \u2014 unless the caller opted into pinOrganizationId because its recordId\n // is not unique across organizations.\n await em.nativeDelete(CustomFieldValue, { entityId, recordId, fieldKey, ...reconcileScopeFilter })\n for (const val of arr) {\n const col: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(val)\n const cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n clearValueColumns(cf)\n const stored = encrypted\n ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache, { entityId, fieldKey })\n : val\n switch (col) {\n case 'valueText': cf.valueText = stored == null ? null : String(stored); break\n case 'valueMultiline': cf.valueMultiline = stored == null ? null : String(stored); break\n case 'valueInt': cf.valueInt = stored == null ? null : Number(stored); break\n case 'valueFloat': cf.valueFloat = stored == null ? null : Number(stored); break\n case 'valueBool': cf.valueBool = stored == null ? null : Boolean(stored); break\n default: cf.valueText = stored == null ? null : String(stored); break\n }\n toPersist.push(cf)\n }\n continue\n }\n\n // Same reconciliation as the multi-value branch (see tenantScopeFilter): load every\n // reachable row for the logical key, reuse the one already in the caller's exact\n // scope and drop the rest, so a row left behind by a previous organization \u2014 and\n // same-scope duplicates from two writes that both missed the lookup \u2014 cannot\n // survive (#5970). Picking the survivor in JS rather than narrowing the query keeps\n // NULL organizations out of SQL three-valued logic, where an inequality predicate\n // would silently spare exactly the stale rows this is meant to remove.\n const existingRows = await em.find(CustomFieldValue, { entityId, recordId, fieldKey, ...reconcileScopeFilter })\n // A soft-deleted row is never the survivor: writing the new value onto it would leave\n // deleted_at set, hiding the field from loadCustomFieldValues while the live row it\n // replaced goes into staleIds. Tombstones still get deleted as stale \u2014 the Query\n // Engine's join does not filter deleted_at, so one left behind would answer reads.\n let cf = existingRows.find((row) => !row.deletedAt\n && (row.organizationId ?? null) === organizationId\n && (row.tenantId ?? null) === tenantId) ?? null\n const value: Primitive = collapseToSingle\n ? pickSingleValue(raw as Primitive[], cf ? await decryptCustomFieldValue(valueFromRow(cf), tenantId, encrypted ? getEncryptionService() : null, encryptionCache, { kind: def?.kind ?? null }) : undefined)\n : raw as Primitive\n const column: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(value)\n const storedValue = encrypted\n ? await encryptCustomFieldValue(value, tenantId, getEncryptionService(), encryptionCache, { entityId, fieldKey })\n : value\n const staleIds = existingRows.filter((row) => row !== cf).map((row) => row.id)\n if (staleIds.length) await em.nativeDelete(CustomFieldValue, { id: { $in: staleIds } })\n if (!cf) {\n cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n toPersist.push(cf)\n }\n clearValueColumns(cf)\n switch (column) {\n case 'valueText':\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueMultiline':\n cf.valueMultiline = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueInt':\n cf.valueInt = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueFloat':\n cf.valueFloat = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueBool':\n cf.valueBool = (storedValue as Primitive) == null ? null : Boolean(storedValue as Primitive)\n break\n default:\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n }\n }\n\n if (toPersist.length) em.persist(toPersist)\n await em.flush()\n if (ownCustomFieldTransaction) await txEm.commit!()\n } catch (err) {\n if (ownCustomFieldTransaction) {\n try { await txEm.rollback!() } catch { /* surface the original error, not a rollback failure */ }\n }\n throw err\n }\n // Emit hook for indexing if requested (outside CRUD flows). Runs AFTER the\n // transaction commits so consumers observe the persisted rows.\n try {\n if (typeof opts.onChanged === 'function') {\n await opts.onChanged({ entityId, recordId, organizationId, tenantId })\n }\n } catch {\n // Non-blocking\n }\n}\n"],
5
- "mappings": "AAEA,SAAS,yBAAyB,yBAAyB,sCAAsC;AACjG;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAyB,wBAAwB;AACjD,SAAS,iCAAiC;AAC1C,SAAS,mCAAmC;AAC5C,SAAS,qCAAqC;AAG9C,eAAsB,kCACpB,IACA,MACkC;AAClC,MAAI,CAAC,8BAA8B,KAAK,OAAO,KAAK,KAAK,GAAG,EAAE,MAAM,CAAC,QAAQ,QAAQ,IAAI,EAAG,QAAO,KAAK;AACxG,QAAM,cAAc,MAAM,0BAA0B,IAAI,IAAI;AAC5D,QAAM,SAAS,MAAM,KAAK,YAAY,OAAO,GAAG,CAAC,gBAAgB;AAAA,IAC/D,KAAK,WAAW;AAAA,IAChB,MAAM,WAAW;AAAA,IACjB,WAAW,QAAQ,WAAW,YAAY,SAAS;AAAA,EACrD,EAAE;AACF,SAAO,4BAA4B,KAAK,KAAK,QAAQ,KAAK,UAAU,+BAA+B,IAAI,KAAK,iBAAiB,CAAC;AAChI;AA8BA,SAAS,eAAe,MAAsC;AAC5D,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,kBAAkB,GAAsC;AAC/D,MAAI,MAAM,QAAQ,MAAM,OAAW,QAAO;AAC1C,MAAI,OAAO,MAAM,UAAW,QAAO;AACnC,MAAI,OAAO,MAAM,SAAU,QAAO,OAAO,UAAU,CAAC,IAAI,aAAa;AACrE,SAAO;AACT;AAGA,SAAS,kBAAkB,IAAsB;AAC/C,KAAG,YAAY;AACf,KAAG,iBAAiB;AACpB,KAAG,WAAW;AACd,KAAG,aAAa;AAChB,KAAG,YAAY;AACjB;AAEA,SAAS,aAAa,KAAkC;AACtD,MAAI,IAAI,kBAAkB,KAAM,QAAO,IAAI;AAC3C,MAAI,IAAI,aAAa,KAAM,QAAO,IAAI;AACtC,MAAI,IAAI,YAAY,KAAM,QAAO,IAAI;AACrC,MAAI,IAAI,cAAc,KAAM,QAAO,IAAI;AACvC,MAAI,IAAI,aAAa,KAAM,QAAO,IAAI;AACtC,SAAO;AACT;AAIA,SAAS,gBAAgB,YAAyB,cAAkC;AAClF,MAAI,gBAAgB,MAAM;AACxB,UAAM,OAAO,WAAW,KAAK,CAAC,cAAc,aAAa,QAAQ,OAAO,SAAS,MAAM,OAAO,YAAY,CAAC;AAC3G,QAAI,SAAS,OAAW,QAAO;AAAA,EACjC;AACA,QAAM,UAAU,WAAW,OAAO,CAAC,cAAc,aAAa,IAAI;AAClE,SAAO,QAAQ,SAAS,QAAQ,QAAQ,SAAS,CAAC,IAAI;AACxD;AAEA,eAAsB,sBACpB,IACA,MACe;AACf,QAAM,EAAE,UAAU,UAAU,OAAO,IAAI;AACvC,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,aAAa,KAAK,eAAe;AAEvC,MAAI;AACJ,MAAI,YAAY;AACd,gBAAY,MAAM,0BAA0B,IAAI,EAAE,UAAU,gBAAgB,SAAS,CAAC;AAAA,EACxF;AAEA,QAAM,YAAgC,CAAC;AACvC,MAAI;AACJ,QAAM,kBAAkB,oBAAI,IAAkC;AAC9D,QAAM,uBAAuB,MAAM;AACjC,QAAI,sBAAsB,OAAW,QAAO;AAC5C,wBAAoB,+BAA+B,IAAW,KAAK,iBAAiB;AACpF,WAAO;AAAA,EACT;AAsBA,QAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,EAAE,UAAU,KAAK,CAAC,EAAE;AACpE,QAAM,oBAAoB,KAAK,sBAAsB;AACrD,QAAM,uBAAuB,oBACzB,EAAE,gBAAgB,GAAG,kBAAkB,IACvC;AACJ,QAAM,OAAO,OAAO,KAAK,MAAM;AAC/B,QAAM,kBAAkB,KAAK,OAAO,CAAC,QAAQ,OAAO,GAAG,MAAM,MAAS,EAAE;AACxE,MAAI,cAAc,kBAAkB,kCAAkC;AACpE,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AAYA,QAAM,OAAO;AAMb,QAAM,YACJ,OAAO,KAAK,UAAU,cACtB,OAAO,KAAK,WAAW,cACvB,OAAO,KAAK,aAAa,cACzB,OAAO,KAAK,oBAAoB;AAClC,QAAM,4BAA4B,aAAa,CAAC,KAAK,gBAAiB;AACtE,MAAI,0BAA2B,OAAM,KAAK,MAAO;AACjD,MAAI;AACJ,eAAW,YAAY,MAAM;AAC3B,YAAM,MAAM,OAAO,QAAQ;AAC3B,UAAI,QAAQ,OAAW;AAEvB,YAAM,MAAM,WAAW,IAAI,QAAQ;AACnC,YAAM,YAAY,QAAQ,KAAK,cAAe,IAAY,YAAY,SAAS;AAM/E,YAAM,mBAAmB,MAAM,QAAQ,GAAG,KAAK,QAAQ,GAAG,KAAM,IAAY,YAAY,UAAU;AAClG,YAAM,UAAU,MAAM,QAAQ,GAAG,KAAK,CAAC;AAOvC,UAAI,SAAS;AACX,cAAM,MAAM;AAOZ,cAAM,GAAG,aAAa,kBAAkB,EAAE,UAAU,UAAU,UAAU,GAAG,qBAAqB,CAAC;AACjG,mBAAW,OAAO,KAAK;AACrB,gBAAM,MAA8B,YAAY,cAAc,MAAM,eAAe,IAAI,IAAI,IAAI,kBAAkB,GAAG;AACpH,gBAAMA,MAAK,GAAG,OAAO,kBAAkB,EAAE,UAAU,UAAU,gBAAgB,UAAU,UAAU,WAAW,oBAAI,KAAK,EAAE,CAAC;AACxH,4BAAkBA,GAAE;AACpB,gBAAM,SAAS,YACX,MAAM,wBAAwB,KAAK,UAAU,qBAAqB,GAAG,iBAAiB,EAAE,UAAU,SAAS,CAAC,IAC5G;AACJ,kBAAQ,KAAK;AAAA,YACX,KAAK;AAAa,cAAAA,IAAG,YAAY,UAAU,OAAO,OAAO,OAAO,MAAM;AAAG;AAAA,YACzE,KAAK;AAAkB,cAAAA,IAAG,iBAAiB,UAAU,OAAO,OAAO,OAAO,MAAM;AAAG;AAAA,YACnF,KAAK;AAAY,cAAAA,IAAG,WAAW,UAAU,OAAO,OAAO,OAAO,MAAM;AAAG;AAAA,YACvE,KAAK;AAAc,cAAAA,IAAG,aAAa,UAAU,OAAO,OAAO,OAAO,MAAM;AAAG;AAAA,YAC3E,KAAK;AAAa,cAAAA,IAAG,YAAY,UAAU,OAAO,OAAO,QAAQ,MAAM;AAAG;AAAA,YAC1E;AAAS,cAAAA,IAAG,YAAY,UAAU,OAAO,OAAO,OAAO,MAAM;AAAG;AAAA,UAClE;AACA,oBAAU,KAAKA,GAAE;AAAA,QACnB;AACA;AAAA,MACF;AASA,YAAM,eAAe,MAAM,GAAG,KAAK,kBAAkB,EAAE,UAAU,UAAU,UAAU,GAAG,qBAAqB,CAAC;AAK9G,UAAI,KAAK,aAAa,KAAK,CAAC,QAAQ,CAAC,IAAI,cACnC,IAAI,kBAAkB,UAAU,mBAChC,IAAI,YAAY,UAAU,QAAQ,KAAK;AAC7C,YAAM,QAAmB,mBACrB,gBAAgB,KAAoB,KAAK,MAAM,wBAAwB,aAAa,EAAE,GAAG,UAAU,YAAY,qBAAqB,IAAI,MAAM,iBAAiB,EAAE,MAAM,KAAK,QAAQ,KAAK,CAAC,IAAI,MAAS,IACvM;AACJ,YAAM,SAAiC,YAAY,cAAc,MAAM,eAAe,IAAI,IAAI,IAAI,kBAAkB,KAAK;AACzH,YAAM,cAAc,YAChB,MAAM,wBAAwB,OAAO,UAAU,qBAAqB,GAAG,iBAAiB,EAAE,UAAU,SAAS,CAAC,IAC9G;AACJ,YAAM,WAAW,aAAa,OAAO,CAAC,QAAQ,QAAQ,EAAE,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;AAC7E,UAAI,SAAS,OAAQ,OAAM,GAAG,aAAa,kBAAkB,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;AACtF,UAAI,CAAC,IAAI;AACP,aAAK,GAAG,OAAO,kBAAkB,EAAE,UAAU,UAAU,gBAAgB,UAAU,UAAU,WAAW,oBAAI,KAAK,EAAE,CAAC;AAClH,kBAAU,KAAK,EAAE;AAAA,MACnB;AACA,wBAAkB,EAAE;AACpB,cAAQ,QAAQ;AAAA,QACd,KAAK;AACH,aAAG,YAAa,eAA6B,OAAO,OAAO,OAAO,WAAwB;AAC1F;AAAA,QACF,KAAK;AACH,aAAG,iBAAkB,eAA6B,OAAO,OAAO,OAAO,WAAwB;AAC/F;AAAA,QACF,KAAK;AACH,aAAG,WAAY,eAA6B,OAAO,OAAO,OAAO,WAAwB;AACzF;AAAA,QACF,KAAK;AACH,aAAG,aAAc,eAA6B,OAAO,OAAO,OAAO,WAAwB;AAC3F;AAAA,QACF,KAAK;AACH,aAAG,YAAa,eAA6B,OAAO,OAAO,QAAQ,WAAwB;AAC3F;AAAA,QACF;AACE,aAAG,YAAa,eAA6B,OAAO,OAAO,OAAO,WAAwB;AAC1F;AAAA,MACJ;AAAA,IACF;AAEA,QAAI,UAAU,OAAQ,IAAG,QAAQ,SAAS;AAC1C,UAAM,GAAG,MAAM;AACb,QAAI,0BAA2B,OAAM,KAAK,OAAQ;AAAA,EACpD,SAAS,KAAK;AACZ,QAAI,2BAA2B;AAC7B,UAAI;AAAE,cAAM,KAAK,SAAU;AAAA,MAAE,QAAQ;AAAA,MAA2D;AAAA,IAClG;AACA,UAAM;AAAA,EACR;AAGA,MAAI;AACF,QAAI,OAAO,KAAK,cAAc,YAAY;AACxC,YAAM,KAAK,UAAU,EAAE,UAAU,UAAU,gBAAgB,SAAS,CAAC;AAAA,IACvE;AAAA,EACF,QAAQ;AAAA,EAER;AACF;",
6
- "names": ["cf"]
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { decryptCustomFieldValue, encryptCustomFieldValue, resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport {\n MAX_CUSTOM_FIELD_KEYS_PER_RECORD,\n TOO_MANY_CUSTOM_FIELDS_ERROR,\n} from '@open-mercato/shared/modules/entities/validation'\nimport { CustomFieldDef, CustomFieldValue } from '../data/entities'\nimport { loadScopedCustomFieldDefs } from './scoped-field-defs'\nimport { encryptCustomEntityDocument } from '@open-mercato/shared/lib/encryption/customEntityDoc'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\n\n/** @internal */\nexport async function encryptCustomEntityRecordDocument(\n em: EntityManager,\n opts: { entityId: string; organizationId: string | null; tenantId: string | null; doc: Record<string, unknown>; encryptionService?: TenantDataEncryptionService | null },\n): Promise<Record<string, unknown>> {\n if (!isTenantDataEncryptionEnabled() || Object.keys(opts.doc).every((key) => key === 'id')) return opts.doc\n const definitions = await loadScopedCustomFieldDefs(em, opts)\n const fields = Array.from(definitions.values(), (definition) => ({\n key: definition.key,\n kind: definition.kind,\n encrypted: Boolean(definition.configJson?.encrypted),\n }))\n return encryptCustomEntityDocument(opts.doc, fields, opts.tenantId, resolveTenantEncryptionService(em, opts.encryptionService))\n}\n\ntype Primitive = string | number | boolean | null | undefined\ntype PrimitiveOrArray = Primitive | Primitive[]\n\nexport type SetRecordCustomFieldsOptions = {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, PrimitiveOrArray>\n // When true (default), try to use field definitions to decide storage column\n preferDefs?: boolean\n // Optional: notify external systems (e.g., indexing) when values changed\n onChanged?: (payload: { entityId: string; recordId: string; organizationId: string | null; tenantId: string | null }) => Promise<void> | void\n // Optional: re-use an existing tenant encryption service instance\n encryptionService?: TenantDataEncryptionService | null\n // When true, additionally scope the reconciling delete/lookup by organizationId,\n // restoring the pre-#5970 organization pin. Default false relies on `recordId`\n // being unique across organizations for a given entityId \u2014 true for every\n // ORM-backed entity, where recordId is the record's own primary key. That\n // invariant does not hold for `custom_entities_storage` records written under\n // `ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM`: the table upserts on\n // `(entity_type, entity_id, organization_id)`, so the same id can be a live\n // record in two organizations of one tenant, and an organization-agnostic\n // delete could remove another organization's values (#6034 review). Callers\n // writing those records must pass pinOrganizationId: true.\n pinOrganizationId?: boolean\n}\n\nfunction columnFromKind(kind: string): keyof CustomFieldValue {\n switch (kind) {\n case 'text':\n case 'select':\n case 'currency':\n case 'dictionary':\n case 'phone':\n return 'valueText'\n case 'multiline':\n return 'valueMultiline'\n case 'integer':\n return 'valueInt'\n case 'float':\n return 'valueFloat'\n case 'boolean':\n return 'valueBool'\n default:\n return 'valueText'\n }\n}\n\nfunction columnFromJsValue(v: Primitive): keyof CustomFieldValue {\n if (v === null || v === undefined) return 'valueText'\n if (typeof v === 'boolean') return 'valueBool'\n if (typeof v === 'number') return Number.isInteger(v) ? 'valueInt' : 'valueFloat'\n return 'valueText'\n}\n\n// Clears all value columns to avoid leftovers on update\nfunction clearValueColumns(cf: CustomFieldValue) {\n cf.valueText = null\n cf.valueMultiline = null\n cf.valueInt = null\n cf.valueFloat = null\n cf.valueBool = null\n}\n\nfunction valueFromRow(row: CustomFieldValue): Primitive {\n if (row.valueMultiline != null) return row.valueMultiline\n if (row.valueText != null) return row.valueText\n if (row.valueInt != null) return row.valueInt\n if (row.valueFloat != null) return row.valueFloat\n if (row.valueBool != null) return row.valueBool\n return null\n}\n\n// Keeps the value the caller's own scope already holds when the echoed array contains it,\n// so a save that never touched the field does not swap in another organization's value.\nfunction pickSingleValue(candidates: Primitive[], currentValue: unknown): Primitive {\n if (currentValue != null) {\n const kept = candidates.find((candidate) => candidate != null && String(candidate) === String(currentValue))\n if (kept !== undefined) return kept\n }\n const present = candidates.filter((candidate) => candidate != null)\n return present.length ? present[present.length - 1] : null\n}\n\nexport async function setRecordCustomFields(\n em: EntityManager,\n opts: SetRecordCustomFieldsOptions,\n): Promise<void> {\n const { entityId, recordId, values } = opts\n const organizationId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const preferDefs = opts.preferDefs !== false\n\n let defsByKey: Map<string, CustomFieldDef> | undefined\n if (preferDefs) {\n defsByKey = await loadScopedCustomFieldDefs(em, { entityId, organizationId, tenantId })\n }\n\n const toPersist: CustomFieldValue[] = []\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 as any, opts.encryptionService)\n return encryptionService\n }\n // A write owns every value row a reader in ITS OWN tenant scope could return for this\n // record: same logical key (entityId, recordId, fieldKey), any organization, and either\n // the caller's tenant or the instance-global NULL tenant \u2014 exactly what the Query Engine\n // matches. Organization is left out because the Query Engine's value join does not filter\n // it (engine.ts, the cf join and the EXISTS subquery), so a row from a scope the record\n // has left is indistinguishable from the live one (#5970). The query-index reader does\n // scope organization_id, but it never sees another organization's row for a record it is\n // indexing, so dropping the column here cannot widen what it returns.\n //\n // This relies on recordId being unique across organizations and tenants for a given\n // entityId \u2014 true of every ORM-backed caller, which passes the record's own primary key.\n // A caller that passed a per-organization natural key \u2014 or a recordId that is only unique\n // WITHIN an organization, as custom_entities_storage's EAV backcompat writes are \u2014 would\n // have one organization's write delete another's live rows. Those callers opt into\n // pinOrganizationId (see SetRecordCustomFieldsOptions) to restore the organization pin.\n //\n // Scope of the delete, precisely: another TENANT's own rows are never touched. Rows with a\n // NULL tenant are, deliberately \u2014 they are instance-global and answer every tenant's reads,\n // so leaving one behind would reproduce the very duplicate this reconciles. Spelled as an\n // explicit $or rather than `$in: [tenantId, null]` because SQL `IN (NULL)` never matches a\n // NULL row.\n const tenantScopeFilter = { $or: [{ tenantId }, { tenantId: null }] }\n const pinOrganizationId = opts.pinOrganizationId === true\n const reconcileScopeFilter = pinOrganizationId\n ? { organizationId, ...tenantScopeFilter }\n : tenantScopeFilter\n const keys = Object.keys(values)\n const presentKeyCount = keys.filter((key) => values[key] !== undefined).length\n if (preferDefs && presentKeyCount > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {\n throw new Error(TOO_MANY_CUSTOM_FIELDS_ERROR)\n }\n\n // Run the per-key delete+insert work inside ONE database transaction so a\n // multi-value replacement is atomic and isolated. The array branch deletes the\n // existing rows for a key and inserts the replacements; without an enclosing\n // transaction those can land in separate commit boundaries under MikroORM's\n // FlushMode.AUTO (a query elsewhere in the unit auto-flushes part of the work),\n // which intermittently left the field with the delete applied but the inserts\n // missing \u2014 the multi-select EDIT reverted to []. The single commit below makes\n // it all-or-nothing. We only open our own transaction when the caller has not\n // already started one (commands fork the request em and may run setCustomFields\n // outside their own withAtomicFlush tx); join an ambient transaction otherwise.\n const txEm = em as {\n begin?: () => Promise<void>\n commit?: () => Promise<void>\n rollback?: () => Promise<void>\n isInTransaction?: () => boolean\n }\n const txCapable =\n typeof txEm.begin === 'function' &&\n typeof txEm.commit === 'function' &&\n typeof txEm.rollback === 'function' &&\n typeof txEm.isInTransaction === 'function'\n const ownCustomFieldTransaction = txCapable && !txEm.isInTransaction!()\n if (ownCustomFieldTransaction) await txEm.begin!()\n try {\n for (const fieldKey of keys) {\n const raw = values[fieldKey]\n if (raw === undefined) continue\n\n const def = defsByKey?.get(fieldKey)\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n // A single-value definition never stores an array. One arrives when the detail read\n // returned the duplicate rows #5970 left behind as an array and a form echoed it back\n // unchanged; treating it as a multi-value replacement re-creates every duplicate under\n // the caller's organization (#6468). Collapse it to one value so the scalar branch\n // below reconciles the duplicates away.\n const collapseToSingle = Array.isArray(raw) && Boolean(def) && (def as any).configJson?.multi !== true\n const isArray = (Array.isArray(raw) && !collapseToSingle)\n || (raw === null && def?.configJson?.multi === true)\n // When array (multi-value): replace all existing rows for the key. Delete\n // first, then create replacements, all inside the transaction opened above.\n // Creating rows before a native delete can auto-flush and delete the new\n // values; mixing em.remove(stale) with new rows for the same EAV scope was\n // observed to commit an empty set under MikroORM v7. The explicit order keeps\n // the replacement atomic without letting old-row cleanup target new rows.\n if (isArray) {\n const arr = Array.isArray(raw) ? raw.filter((value) => value != null) : []\n // Reconciling delete (see tenantScopeFilter/reconcileScopeFilter): pinning\n // organizationId here left the rows written under the record's PREVIOUS\n // organization alive next to the replacements, and the Query Engine's value join\n // does not filter organization_id, so both generations answered the same lookup\n // (#5970) \u2014 unless the caller opted into pinOrganizationId because its recordId\n // is not unique across organizations.\n await em.nativeDelete(CustomFieldValue, { entityId, recordId, fieldKey, ...reconcileScopeFilter })\n for (const val of arr) {\n const col: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(val)\n const cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n clearValueColumns(cf)\n const stored = encrypted\n ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache, { entityId, fieldKey })\n : val\n switch (col) {\n case 'valueText': cf.valueText = stored == null ? null : String(stored); break\n case 'valueMultiline': cf.valueMultiline = stored == null ? null : String(stored); break\n case 'valueInt': cf.valueInt = stored == null ? null : Number(stored); break\n case 'valueFloat': cf.valueFloat = stored == null ? null : Number(stored); break\n case 'valueBool': cf.valueBool = stored == null ? null : Boolean(stored); break\n default: cf.valueText = stored == null ? null : String(stored); break\n }\n toPersist.push(cf)\n }\n continue\n }\n\n // Same reconciliation as the multi-value branch (see tenantScopeFilter): load every\n // reachable row for the logical key, reuse the one already in the caller's exact\n // scope and drop the rest, so a row left behind by a previous organization \u2014 and\n // same-scope duplicates from two writes that both missed the lookup \u2014 cannot\n // survive (#5970). Picking the survivor in JS rather than narrowing the query keeps\n // NULL organizations out of SQL three-valued logic, where an inequality predicate\n // would silently spare exactly the stale rows this is meant to remove.\n const existingRows = await em.find(CustomFieldValue, { entityId, recordId, fieldKey, ...reconcileScopeFilter })\n // A soft-deleted row is never the survivor: writing the new value onto it would leave\n // deleted_at set, hiding the field from loadCustomFieldValues while the live row it\n // replaced goes into staleIds. Tombstones still get deleted as stale \u2014 the Query\n // Engine's join does not filter deleted_at, so one left behind would answer reads.\n let cf = existingRows.find((row) => !row.deletedAt\n && (row.organizationId ?? null) === organizationId\n && (row.tenantId ?? null) === tenantId) ?? null\n const value: Primitive = collapseToSingle\n ? pickSingleValue(raw as Primitive[], cf ? await decryptCustomFieldValue(valueFromRow(cf), tenantId, encrypted ? getEncryptionService() : null, encryptionCache, { kind: def?.kind ?? null }) : undefined)\n : raw as Primitive\n const column: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(value)\n const storedValue = encrypted\n ? await encryptCustomFieldValue(value, tenantId, getEncryptionService(), encryptionCache, { entityId, fieldKey })\n : value\n const staleIds = existingRows.filter((row) => row !== cf).map((row) => row.id)\n if (staleIds.length) await em.nativeDelete(CustomFieldValue, { id: { $in: staleIds } })\n if (!cf) {\n cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n toPersist.push(cf)\n }\n clearValueColumns(cf)\n switch (column) {\n case 'valueText':\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueMultiline':\n cf.valueMultiline = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueInt':\n cf.valueInt = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueFloat':\n cf.valueFloat = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueBool':\n cf.valueBool = (storedValue as Primitive) == null ? null : Boolean(storedValue as Primitive)\n break\n default:\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n }\n }\n\n if (toPersist.length) em.persist(toPersist)\n await em.flush()\n if (ownCustomFieldTransaction) await txEm.commit!()\n } catch (err) {\n if (ownCustomFieldTransaction) {\n try { await txEm.rollback!() } catch { /* surface the original error, not a rollback failure */ }\n }\n throw err\n }\n // Emit hook for indexing if requested (outside CRUD flows). Runs AFTER the\n // transaction commits so consumers observe the persisted rows.\n try {\n if (typeof opts.onChanged === 'function') {\n await opts.onChanged({ entityId, recordId, organizationId, tenantId })\n }\n } catch {\n // Non-blocking\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["value", "cf"]
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { resolveQueryIndexSourceTableMetadata } from "./subscriber-scope.js";
2
3
  import { resolveEntityTableName as resolveFallbackEntityTableName } from "@open-mercato/shared/lib/query/engine";
3
4
  import { resolveTenantEncryptionService } from "@open-mercato/shared/lib/encryption/customFieldValues";
@@ -9,6 +10,8 @@ import {
9
10
  } from "@open-mercato/shared/lib/crud/custom-field-definition-index";
10
11
  import { sql } from "kysely";
11
12
  import { createLogger } from "@open-mercato/shared/lib/logger";
13
+ import { recordIndexerError } from "@open-mercato/shared/lib/indexers/error-log";
14
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
12
15
  import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
13
16
  import { replaceSearchTokensForRecord, deleteSearchTokensForRecord } from "./search-tokens.js";
14
17
  import { attachAggregateSearchField, rebuildAggregateSearchField } from "./document.js";
@@ -17,6 +20,13 @@ function resolveEntityTableName(em, entity) {
17
20
  return resolveQueryIndexSourceTableMetadata(em, entity)?.table ?? resolveFallbackEntityTableName(em, entity);
18
21
  }
19
22
  const logger = createLogger("query_index").child({ component: "indexer" });
23
+ function reportTranslationsFailure(error, code) {
24
+ try {
25
+ getTelemetryRuntime()?.reportError(error, { module: "query_index", code });
26
+ } catch (telemetryError) {
27
+ logger.warn("Failed to report index translations error", { err: telemetryError });
28
+ }
29
+ }
20
30
  async function buildIndexDoc(em, params) {
21
31
  const db = em.getKysely();
22
32
  const baseTable = resolveEntityTableName(em, params.entityType);
@@ -129,6 +139,8 @@ async function buildIndexDoc(em, params) {
129
139
  const fieldKey = key.slice(3);
130
140
  doc[key] = multiKeys.has(fieldKey) || arr.length > 1 ? arr : arr[0];
131
141
  }
142
+ const translationsSavepoint = db.isTransaction ? `om_translations_${randomUUID()}` : null;
143
+ if (translationsSavepoint) await sql`SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db);
132
144
  try {
133
145
  const translationRow = await db.selectFrom("entity_translations").select(["translations"]).where("entity_type", "=", params.entityType).where("entity_id", "=", String(params.recordId)).where(sql`tenant_id is not distinct from ${params.tenantId ?? null}`).where(sql`organization_id is not distinct from ${params.organizationId ?? null}`).executeTakeFirst();
134
146
  if (translationRow?.translations && typeof translationRow.translations === "object") {
@@ -141,7 +153,33 @@ async function buildIndexDoc(em, params) {
141
153
  }
142
154
  }
143
155
  }
144
- } catch {
156
+ } catch (translationsError) {
157
+ if (translationsSavepoint) {
158
+ await sql`ROLLBACK TO SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db);
159
+ }
160
+ reportTranslationsFailure(translationsError, "query_index.translations_read_failed");
161
+ await recordIndexerError(
162
+ { db },
163
+ {
164
+ source: "query_index",
165
+ handler: "query_index:build-index-doc:translations",
166
+ error: translationsError,
167
+ entityType: params.entityType,
168
+ recordId: params.recordId,
169
+ tenantId: params.tenantId ?? null,
170
+ organizationId: params.organizationId ?? null
171
+ }
172
+ ).catch((loggingError) => reportTranslationsFailure(loggingError, "query_index.translations_log_failed"));
173
+ } finally {
174
+ if (translationsSavepoint) {
175
+ try {
176
+ await sql`RELEASE SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db);
177
+ } catch (loggingError) {
178
+ await sql`ROLLBACK TO SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db);
179
+ await sql`RELEASE SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db);
180
+ reportTranslationsFailure(loggingError, "query_index.translations_log_failed");
181
+ }
182
+ }
145
183
  }
146
184
  await applyIndexDocEnrichers(db, params.entityType, [{
147
185
  recordId: String(params.recordId),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/lib/indexer.ts"],
4
- "sourcesContent": ["import { resolveQueryIndexSourceTableMetadata } from './subscriber-scope'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveEntityTableName as resolveFallbackEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { decryptIndexDocForSearch, encryptIndexDocForStorage } from '@open-mercato/shared/lib/encryption/indexDoc'\nimport {\n buildCustomFieldDefinitionIndexFromRows,\n normalizeDefinitionKey,\n selectDefinitionForRecord,\n type CustomFieldDefinitionRow,\n} from '@open-mercato/shared/lib/crud/custom-field-definition-index'\nimport { type Kysely, type Transaction, sql } from 'kysely'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { replaceSearchTokensForRecord, deleteSearchTokensForRecord } from './search-tokens'\nimport { attachAggregateSearchField, rebuildAggregateSearchField } from './document'\nimport { applyIndexDocEnrichers } from './doc-enrichers'\n\nfunction resolveEntityTableName(em: EntityManager, entity: string): string {\n return resolveQueryIndexSourceTableMetadata(em, entity)?.table ?? resolveFallbackEntityTableName(em, entity)\n}\n\nconst logger = createLogger('query_index').child({ component: 'indexer' })\n\ntype QueryIndexExecutor = Kysely<any> | Transaction<any>\n\ntype BuildDocParams = {\n entityType: string // '<module>:<entity>'\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\nexport async function buildIndexDoc(em: EntityManager, params: BuildDocParams): Promise<Record<string, any> | null> {\n const db = (em as any).getKysely()\n const baseTable = resolveEntityTableName(em, params.entityType)\n\n // Fetch base row\n const baseRow = await db\n .selectFrom(baseTable as any)\n .selectAll()\n .where('id' as any, '=', params.recordId)\n .executeTakeFirst() as Record<string, any> | undefined\n if (!baseRow) return null\n const docSources: Array<Record<string, any>> = []\n\n // Attach the core customer entity when indexing customer profiles so search tokens see the combined row\n let parentEntityRow: Record<string, any> | null = null\n if (params.entityType === 'customers:customer_person_profile' || params.entityType === 'customers:customer_company_profile') {\n const entityId = (baseRow as any).entity_id ?? (baseRow as any).entityId\n if (entityId) {\n const entityRow = await db\n .selectFrom('customer_entities' as any)\n .selectAll()\n .where('id' as any, '=', entityId)\n .executeTakeFirst() as Record<string, any> | undefined\n if (entityRow) {\n docSources.push(entityRow)\n parentEntityRow = entityRow\n }\n }\n }\n void parentEntityRow\n\n // Build base document (snake_case keys as in DB)\n let doc: Record<string, any> = {}\n docSources.push(baseRow)\n for (const source of docSources) {\n for (const [k, v] of Object.entries(source)) doc[k] = v\n }\n\n // Attach custom fields under flat keys 'cf:<key>'\n let cfQuery = db\n .selectFrom('custom_field_values' as any)\n .select([\n 'field_key' as any,\n 'value_text' as any,\n 'value_multiline' as any,\n 'value_int' as any,\n 'value_float' as any,\n 'value_bool' as any,\n ])\n .where('entity_id' as any, '=', params.entityType)\n .where('record_id' as any, '=', String(params.recordId))\n\n if (params.organizationId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', params.organizationId),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('organization_id' as any, 'is', null)\n }\n\n if (params.tenantId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', params.tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('tenant_id' as any, 'is', null)\n }\n\n const cfRows = await cfQuery.execute() as Array<Record<string, any>>\n\n const cfMap: Record<string, any[]> = {}\n for (const r of cfRows) {\n const key = String(r.field_key)\n const cfKey = `cf:${key}`\n const val = r.value_bool ?? r.value_int ?? r.value_float ?? r.value_text ?? r.value_multiline ?? null\n if (!cfMap[cfKey]) cfMap[cfKey] = []\n cfMap[cfKey].push(val)\n }\n\n const multiKeys = new Set<string>()\n const fieldKeys = Object.keys(cfMap).map((key) => key.slice(3))\n if (fieldKeys.length > 0) {\n let defQuery = db\n .selectFrom('custom_field_defs' as any)\n .select([\n 'key' as any,\n 'kind' as any,\n 'config_json' as any,\n 'organization_id' as any,\n 'tenant_id' as any,\n 'deleted_at' as any,\n 'updated_at' as any,\n ])\n .where('entity_id' as any, '=', params.entityType)\n .where('key' as any, 'in', fieldKeys)\n .where('is_active' as any, '=', true)\n .where('deleted_at' as any, 'is', null)\n\n if (params.organizationId != null) {\n defQuery = defQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', params.organizationId),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n defQuery = defQuery.where('organization_id' as any, 'is', null)\n }\n\n if (params.tenantId != null) {\n defQuery = defQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', params.tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n defQuery = defQuery.where('tenant_id' as any, 'is', null)\n }\n\n const defRows = await defQuery.execute() as Array<Record<string, any>>\n const definitionIndex = buildCustomFieldDefinitionIndexFromRows(\n defRows.map((row): CustomFieldDefinitionRow => ({\n key: String(row.key ?? ''),\n entityId: params.entityType,\n kind: typeof row.kind === 'string' ? row.kind : null,\n configJson: row.config_json,\n organizationId: row.organization_id ?? null,\n tenantId: row.tenant_id ?? null,\n deletedAt: row.deleted_at ?? null,\n updatedAt: row.updated_at ?? null,\n })),\n { organizationIds: params.organizationId ? [params.organizationId] : [] },\n )\n for (const key of fieldKeys) {\n const definitions = definitionIndex.get(normalizeDefinitionKey(key)) ?? []\n const definition = selectDefinitionForRecord(\n definitions,\n params.organizationId ?? null,\n params.tenantId ?? null,\n )\n if (definition?.multi) multiKeys.add(key)\n }\n }\n\n for (const [key, arr] of Object.entries(cfMap)) {\n // Cardinality belongs to the field definition, not the current row count:\n // a multi field containing one value must still hydrate as an array.\n const fieldKey = key.slice(3)\n doc[key] = multiKeys.has(fieldKey) || arr.length > 1 ? arr : arr[0]\n }\n\n // Attach translations under flat keys 'l10n:{locale}:{field}'\n try {\n const translationRow = await db\n .selectFrom('entity_translations' as any)\n .select(['translations' as any])\n .where('entity_type' as any, '=', params.entityType)\n .where('entity_id' as any, '=', String(params.recordId))\n .where(sql`tenant_id is not distinct from ${params.tenantId ?? null}`)\n .where(sql`organization_id is not distinct from ${params.organizationId ?? null}`)\n .executeTakeFirst() as { translations: Record<string, Record<string, unknown>> | null } | undefined\n\n if (translationRow?.translations && typeof translationRow.translations === 'object') {\n for (const [locale, fields] of Object.entries(translationRow.translations)) {\n if (!fields || typeof fields !== 'object') continue\n for (const [field, value] of Object.entries(fields as Record<string, unknown>)) {\n if (typeof value === 'string' && value.length > 0) {\n doc[`l10n:${locale}:${field}`] = value\n }\n }\n }\n }\n } catch {}\n\n await applyIndexDocEnrichers(db, params.entityType, [{\n recordId: String(params.recordId),\n doc,\n tenantId: params.tenantId ?? null,\n organizationId: params.organizationId ?? null,\n }])\n\n // Kept outside the guard below: a failure while building the aggregate search field is a\n // bug in the aggregation or its configuration, and must surface instead of being mistaken\n // for an encryption failure and silently skipping encryption.\n doc = attachAggregateSearchField(doc, { entityType: params.entityType })\n\n try {\n const encryption = resolveTenantEncryptionService(em as any)\n doc = await encryptIndexDocForStorage(\n params.entityType,\n doc,\n { tenantId: params.tenantId ?? null, organizationId: params.organizationId ?? null },\n encryption,\n )\n } catch (error) {\n // `encryptIndexDocForStorage` already returns the document untouched when encryption is\n // absent or disabled, so a throw here is always a genuine encryption failure. Returning\n // `doc` would hand the caller the plaintext document to write into `entity_indexes`,\n // which must stay encrypted at rest; returning `null` would mean \"record gone\" and make\n // `upsertIndexRow` delete a healthy index row. Fail loudly instead: the projection keeps\n // its previous, correctly encrypted contents and the `query_index.upsert_one` subscriber\n // persists the error via `recordIndexerError`.\n logger.error('Index document encryption failed; refusing to return an unencrypted document', {\n entityType: params.entityType,\n recordId: params.recordId,\n tenantId: params.tenantId ?? null,\n organizationId: params.organizationId ?? null,\n err: error,\n })\n throw error\n }\n\n return doc\n}\n\nexport type UpsertIndexResult = {\n doc: Record<string, any> | null\n existed: boolean\n wasDeleted: boolean\n created: boolean\n revived: boolean\n}\n\nfunction scopeEntityIndexes<QB extends { where: (...args: any[]) => QB }>(\n q: QB,\n args: { entityType: string; recordId: string; organizationId?: string | null; tenantId?: string | null },\n): QB {\n let chain = q.where('entity_type' as any, '=', args.entityType)\n chain = chain.where('entity_id' as any, '=', String(args.recordId))\n chain = args.organizationId == null\n ? chain.where('organization_id' as any, 'is', null as any)\n : chain.where('organization_id' as any, '=', args.organizationId)\n chain = chain.where(sql`tenant_id is not distinct from ${args.tenantId ?? null}`)\n return chain\n}\n\nexport async function upsertIndexRow(\n em: EntityManager,\n args: { entityType: string; recordId: string; organizationId?: string | null; tenantId?: string | null; searchTokenDoc?: Record<string, unknown> | null; deferSearchTokens?: boolean; trx?: QueryIndexExecutor }\n): Promise<UpsertIndexResult> {\n const db = (em as any).getKysely()\n const executor = args.trx ?? db\n\n const existing = await scopeEntityIndexes(\n executor.selectFrom('entity_indexes' as any).select(['id' as any, 'deleted_at' as any]),\n args,\n ).executeTakeFirst() as { id: string; deleted_at: Date | null } | undefined\n\n const existed = !!existing\n const wasDeleted = !!existing && existing.deleted_at != null\n\n const doc = await buildIndexDoc(em, args)\n if (!doc) {\n // When the caller defers token work it owns the matching token cleanup; the\n // projection-row removal below stays synchronous so list reads converge immediately.\n if (!args.deferSearchTokens) {\n try {\n await reindexSearchTokensForRecord(em, {\n entityType: args.entityType,\n recordId: args.recordId,\n organizationId: args.organizationId ?? null,\n tenantId: args.tenantId ?? null,\n doc: null,\n trx: args.trx,\n })\n } catch (error) {\n if (args.trx) throw error\n }\n }\n if (existed) {\n await scopeEntityIndexes(\n executor.deleteFrom('entity_indexes' as any) as any,\n args,\n ).execute()\n }\n return { doc: null, existed, wasDeleted, created: false, revived: false }\n }\n\n const payload = {\n entity_type: args.entityType,\n entity_id: String(args.recordId),\n organization_id: args.organizationId ?? null,\n tenant_id: args.tenantId ?? null,\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n index_version: 1,\n updated_at: sql`now()`,\n deleted_at: null,\n }\n\n // Prefer modern upsert keyed by coalesced org id when available; fallback to update-then-insert\n try {\n await executor\n .insertInto('entity_indexes' as any)\n .values({ ...payload, created_at: sql`now()` } as any)\n .onConflict((oc: any) => oc\n .columns(['entity_type', 'entity_id', 'organization_id_coalesced'])\n .doUpdateSet({\n tenant_id: args.tenantId ?? null,\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n index_version: 1,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any))\n .execute()\n } catch {\n // Fallback for schemas without organization_id_coalesced column/index\n const updated = await scopeEntityIndexes(\n executor.updateTable('entity_indexes' as any).set(payload as any) as any,\n args,\n ).executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n if (!updated || Number(updated.numUpdatedRows ?? 0) === 0) {\n try {\n await executor\n .insertInto('entity_indexes' as any)\n .values({ ...payload, created_at: sql`now()` } as any)\n .execute()\n } catch (error) {\n if (args.trx) throw error\n }\n }\n }\n\n const created = !existed\n const revived = existed && wasDeleted\n // The search-token rebuild (DELETE + chunked INSERT) is the heavy tail of indexing.\n // Callers that defer it (the upsert subscriber) run `reindexSearchTokensForRecord`\n // asynchronously after this projection update so write latency stays bounded.\n if (!args.deferSearchTokens) {\n try {\n await reindexSearchTokensForRecord(em, {\n entityType: args.entityType,\n recordId: args.recordId,\n organizationId: args.organizationId ?? null,\n tenantId: args.tenantId ?? null,\n doc,\n searchTokenDoc: args.searchTokenDoc ?? null,\n trx: args.trx,\n })\n } catch (error) {\n if (args.trx) throw error\n }\n }\n return { doc, existed, wasDeleted, created, revived }\n}\n\n/**\n * Rebuilds (or clears, when `doc` is null) the search-token rows for a single record.\n * This is the asynchronous-friendly tail of `upsertIndexRow`: it does not touch the\n * `entity_indexes` projection that list endpoints read, so it can run out-of-band\n * without making query-index reads inconsistent.\n */\nexport async function reindexSearchTokensForRecord(\n em: EntityManager,\n args: {\n entityType: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n doc: Record<string, any> | null\n searchTokenDoc?: Record<string, unknown> | null\n trx?: QueryIndexExecutor\n },\n): Promise<void> {\n const db = (em as any).getKysely()\n if (!args.doc) {\n await deleteSearchTokensForRecord(db, {\n entityType: args.entityType,\n recordId: args.recordId,\n organizationId: args.organizationId ?? null,\n tenantId: args.tenantId ?? null,\n }, { trx: args.trx })\n return\n }\n const tokenDoc = args.searchTokenDoc ?? (() => {\n const encryption = resolveTenantEncryptionService(em as any)\n const dekKeyCache = new Map<string | null, string | null>()\n return decryptIndexDocForSearch(\n args.entityType,\n args.doc,\n { tenantId: args.tenantId ?? null, organizationId: args.organizationId ?? null },\n encryption,\n dekKeyCache,\n )\n })()\n // Rebuilt on the decrypted document: the aggregate stored in `entity_indexes` was\n // composed from the row as it sits at rest, so for every encryption-mapped field it\n // holds ciphertext no user query can ever match (#5625). `resolveSearchConfig` re-parses\n // the environment on every call, so it is resolved once and threaded into both the\n // rebuild and the token writer \u2014 that also makes the aggregate's blocklist filter and\n // the per-field one provably read the same snapshot, as the batch path already does.\n const searchConfig = resolveSearchConfig()\n const searchDoc = rebuildAggregateSearchField(await tokenDoc, {\n entityType: args.entityType,\n config: searchConfig,\n })\n await replaceSearchTokensForRecord(db, {\n entityType: args.entityType,\n recordId: args.recordId,\n organizationId: args.organizationId ?? null,\n tenantId: args.tenantId ?? null,\n doc: searchDoc,\n config: searchConfig,\n }, { trx: args.trx })\n}\n\nexport async function markDeleted(\n em: EntityManager,\n args: { entityType: string; recordId: string; organizationId?: string | null; tenantId?: string | null; trx?: QueryIndexExecutor }\n): Promise<{ wasActive: boolean }> {\n const db = (em as any).getKysely()\n const executor = args.trx ?? db\n const existing = await scopeEntityIndexes(\n executor.selectFrom('entity_indexes' as any).select(['deleted_at' as any]),\n args,\n ).executeTakeFirst() as { deleted_at: Date | null } | undefined\n\n const wasActive = !!existing && existing.deleted_at == null\n\n if (existing) {\n try {\n await deleteSearchTokensForRecord(db, {\n entityType: args.entityType,\n recordId: args.recordId,\n organizationId: args.organizationId ?? null,\n tenantId: args.tenantId ?? null,\n }, { trx: args.trx })\n } catch (error) {\n if (args.trx) throw error\n }\n await scopeEntityIndexes(\n executor.deleteFrom('entity_indexes' as any) as any,\n args,\n ).execute()\n }\n\n return { wasActive }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport { resolveQueryIndexSourceTableMetadata } from './subscriber-scope'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveEntityTableName as resolveFallbackEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { decryptIndexDocForSearch, encryptIndexDocForStorage } from '@open-mercato/shared/lib/encryption/indexDoc'\nimport {\n buildCustomFieldDefinitionIndexFromRows,\n normalizeDefinitionKey,\n selectDefinitionForRecord,\n type CustomFieldDefinitionRow,\n} from '@open-mercato/shared/lib/crud/custom-field-definition-index'\nimport { type Kysely, type Transaction, sql } from 'kysely'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { replaceSearchTokensForRecord, deleteSearchTokensForRecord } from './search-tokens'\nimport { attachAggregateSearchField, rebuildAggregateSearchField } from './document'\nimport { applyIndexDocEnrichers } from './doc-enrichers'\n\nfunction resolveEntityTableName(em: EntityManager, entity: string): string {\n return resolveQueryIndexSourceTableMetadata(em, entity)?.table ?? resolveFallbackEntityTableName(em, entity)\n}\n\nconst logger = createLogger('query_index').child({ component: 'indexer' })\n\nfunction reportTranslationsFailure(\n error: unknown,\n code: 'query_index.translations_read_failed' | 'query_index.translations_log_failed',\n): void {\n try {\n getTelemetryRuntime()?.reportError(error, { module: 'query_index', code })\n } catch (telemetryError) {\n logger.warn('Failed to report index translations error', { err: telemetryError })\n }\n}\n\ntype QueryIndexExecutor = Kysely<any> | Transaction<any>\n\ntype BuildDocParams = {\n entityType: string // '<module>:<entity>'\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\nexport async function buildIndexDoc(em: EntityManager, params: BuildDocParams): Promise<Record<string, any> | null> {\n const db = (em as any).getKysely()\n const baseTable = resolveEntityTableName(em, params.entityType)\n\n // Fetch base row\n const baseRow = await db\n .selectFrom(baseTable as any)\n .selectAll()\n .where('id' as any, '=', params.recordId)\n .executeTakeFirst() as Record<string, any> | undefined\n if (!baseRow) return null\n const docSources: Array<Record<string, any>> = []\n\n // Attach the core customer entity when indexing customer profiles so search tokens see the combined row\n let parentEntityRow: Record<string, any> | null = null\n if (params.entityType === 'customers:customer_person_profile' || params.entityType === 'customers:customer_company_profile') {\n const entityId = (baseRow as any).entity_id ?? (baseRow as any).entityId\n if (entityId) {\n const entityRow = await db\n .selectFrom('customer_entities' as any)\n .selectAll()\n .where('id' as any, '=', entityId)\n .executeTakeFirst() as Record<string, any> | undefined\n if (entityRow) {\n docSources.push(entityRow)\n parentEntityRow = entityRow\n }\n }\n }\n void parentEntityRow\n\n // Build base document (snake_case keys as in DB)\n let doc: Record<string, any> = {}\n docSources.push(baseRow)\n for (const source of docSources) {\n for (const [k, v] of Object.entries(source)) doc[k] = v\n }\n\n // Attach custom fields under flat keys 'cf:<key>'\n let cfQuery = db\n .selectFrom('custom_field_values' as any)\n .select([\n 'field_key' as any,\n 'value_text' as any,\n 'value_multiline' as any,\n 'value_int' as any,\n 'value_float' as any,\n 'value_bool' as any,\n ])\n .where('entity_id' as any, '=', params.entityType)\n .where('record_id' as any, '=', String(params.recordId))\n\n if (params.organizationId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', params.organizationId),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('organization_id' as any, 'is', null)\n }\n\n if (params.tenantId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', params.tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('tenant_id' as any, 'is', null)\n }\n\n const cfRows = await cfQuery.execute() as Array<Record<string, any>>\n\n const cfMap: Record<string, any[]> = {}\n for (const r of cfRows) {\n const key = String(r.field_key)\n const cfKey = `cf:${key}`\n const val = r.value_bool ?? r.value_int ?? r.value_float ?? r.value_text ?? r.value_multiline ?? null\n if (!cfMap[cfKey]) cfMap[cfKey] = []\n cfMap[cfKey].push(val)\n }\n\n const multiKeys = new Set<string>()\n const fieldKeys = Object.keys(cfMap).map((key) => key.slice(3))\n if (fieldKeys.length > 0) {\n let defQuery = db\n .selectFrom('custom_field_defs' as any)\n .select([\n 'key' as any,\n 'kind' as any,\n 'config_json' as any,\n 'organization_id' as any,\n 'tenant_id' as any,\n 'deleted_at' as any,\n 'updated_at' as any,\n ])\n .where('entity_id' as any, '=', params.entityType)\n .where('key' as any, 'in', fieldKeys)\n .where('is_active' as any, '=', true)\n .where('deleted_at' as any, 'is', null)\n\n if (params.organizationId != null) {\n defQuery = defQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', params.organizationId),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n defQuery = defQuery.where('organization_id' as any, 'is', null)\n }\n\n if (params.tenantId != null) {\n defQuery = defQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', params.tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n defQuery = defQuery.where('tenant_id' as any, 'is', null)\n }\n\n const defRows = await defQuery.execute() as Array<Record<string, any>>\n const definitionIndex = buildCustomFieldDefinitionIndexFromRows(\n defRows.map((row): CustomFieldDefinitionRow => ({\n key: String(row.key ?? ''),\n entityId: params.entityType,\n kind: typeof row.kind === 'string' ? row.kind : null,\n configJson: row.config_json,\n organizationId: row.organization_id ?? null,\n tenantId: row.tenant_id ?? null,\n deletedAt: row.deleted_at ?? null,\n updatedAt: row.updated_at ?? null,\n })),\n { organizationIds: params.organizationId ? [params.organizationId] : [] },\n )\n for (const key of fieldKeys) {\n const definitions = definitionIndex.get(normalizeDefinitionKey(key)) ?? []\n const definition = selectDefinitionForRecord(\n definitions,\n params.organizationId ?? null,\n params.tenantId ?? null,\n )\n if (definition?.multi) multiKeys.add(key)\n }\n }\n\n for (const [key, arr] of Object.entries(cfMap)) {\n // Cardinality belongs to the field definition, not the current row count:\n // a multi field containing one value must still hydrate as an array.\n const fieldKey = key.slice(3)\n doc[key] = multiKeys.has(fieldKey) || arr.length > 1 ? arr : arr[0]\n }\n\n // Attach translations under flat keys 'l10n:{locale}:{field}'\n const translationsSavepoint = db.isTransaction ? `om_translations_${randomUUID()}` : null\n if (translationsSavepoint) await sql`SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db)\n try {\n const translationRow = await db\n .selectFrom('entity_translations' as any)\n .select(['translations' as any])\n .where('entity_type' as any, '=', params.entityType)\n .where('entity_id' as any, '=', String(params.recordId))\n .where(sql`tenant_id is not distinct from ${params.tenantId ?? null}`)\n .where(sql`organization_id is not distinct from ${params.organizationId ?? null}`)\n .executeTakeFirst() as { translations: Record<string, Record<string, unknown>> | null } | undefined\n\n if (translationRow?.translations && typeof translationRow.translations === 'object') {\n for (const [locale, fields] of Object.entries(translationRow.translations)) {\n if (!fields || typeof fields !== 'object') continue\n for (const [field, value] of Object.entries(fields as Record<string, unknown>)) {\n if (typeof value === 'string' && value.length > 0) {\n doc[`l10n:${locale}:${field}`] = value\n }\n }\n }\n }\n } catch (translationsError) {\n if (translationsSavepoint) {\n await sql`ROLLBACK TO SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db)\n }\n reportTranslationsFailure(translationsError, 'query_index.translations_read_failed')\n await recordIndexerError(\n { db },\n {\n source: 'query_index',\n handler: 'query_index:build-index-doc:translations',\n error: translationsError,\n entityType: params.entityType,\n recordId: params.recordId,\n tenantId: params.tenantId ?? null,\n organizationId: params.organizationId ?? null,\n },\n ).catch((loggingError: unknown) => reportTranslationsFailure(loggingError, 'query_index.translations_log_failed'))\n } finally {\n if (translationsSavepoint) {\n try {\n await sql`RELEASE SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db)\n } catch (loggingError) {\n await sql`ROLLBACK TO SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db)\n await sql`RELEASE SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db)\n reportTranslationsFailure(loggingError, 'query_index.translations_log_failed')\n }\n }\n }\n\n await applyIndexDocEnrichers(db, params.entityType, [{\n recordId: String(params.recordId),\n doc,\n tenantId: params.tenantId ?? null,\n organizationId: params.organizationId ?? null,\n }])\n\n // Kept outside the guard below: a failure while building the aggregate search field is a\n // bug in the aggregation or its configuration, and must surface instead of being mistaken\n // for an encryption failure and silently skipping encryption.\n doc = attachAggregateSearchField(doc, { entityType: params.entityType })\n\n try {\n const encryption = resolveTenantEncryptionService(em as any)\n doc = await encryptIndexDocForStorage(\n params.entityType,\n doc,\n { tenantId: params.tenantId ?? null, organizationId: params.organizationId ?? null },\n encryption,\n )\n } catch (error) {\n // `encryptIndexDocForStorage` already returns the document untouched when encryption is\n // absent or disabled, so a throw here is always a genuine encryption failure. Returning\n // `doc` would hand the caller the plaintext document to write into `entity_indexes`,\n // which must stay encrypted at rest; returning `null` would mean \"record gone\" and make\n // `upsertIndexRow` delete a healthy index row. Fail loudly instead: the projection keeps\n // its previous, correctly encrypted contents and the `query_index.upsert_one` subscriber\n // persists the error via `recordIndexerError`.\n logger.error('Index document encryption failed; refusing to return an unencrypted document', {\n entityType: params.entityType,\n recordId: params.recordId,\n tenantId: params.tenantId ?? null,\n organizationId: params.organizationId ?? null,\n err: error,\n })\n throw error\n }\n\n return doc\n}\n\nexport type UpsertIndexResult = {\n doc: Record<string, any> | null\n existed: boolean\n wasDeleted: boolean\n created: boolean\n revived: boolean\n}\n\nfunction scopeEntityIndexes<QB extends { where: (...args: any[]) => QB }>(\n q: QB,\n args: { entityType: string; recordId: string; organizationId?: string | null; tenantId?: string | null },\n): QB {\n let chain = q.where('entity_type' as any, '=', args.entityType)\n chain = chain.where('entity_id' as any, '=', String(args.recordId))\n chain = args.organizationId == null\n ? chain.where('organization_id' as any, 'is', null as any)\n : chain.where('organization_id' as any, '=', args.organizationId)\n chain = chain.where(sql`tenant_id is not distinct from ${args.tenantId ?? null}`)\n return chain\n}\n\nexport async function upsertIndexRow(\n em: EntityManager,\n args: { entityType: string; recordId: string; organizationId?: string | null; tenantId?: string | null; searchTokenDoc?: Record<string, unknown> | null; deferSearchTokens?: boolean; trx?: QueryIndexExecutor }\n): Promise<UpsertIndexResult> {\n const db = (em as any).getKysely()\n const executor = args.trx ?? db\n\n const existing = await scopeEntityIndexes(\n executor.selectFrom('entity_indexes' as any).select(['id' as any, 'deleted_at' as any]),\n args,\n ).executeTakeFirst() as { id: string; deleted_at: Date | null } | undefined\n\n const existed = !!existing\n const wasDeleted = !!existing && existing.deleted_at != null\n\n const doc = await buildIndexDoc(em, args)\n if (!doc) {\n // When the caller defers token work it owns the matching token cleanup; the\n // projection-row removal below stays synchronous so list reads converge immediately.\n if (!args.deferSearchTokens) {\n try {\n await reindexSearchTokensForRecord(em, {\n entityType: args.entityType,\n recordId: args.recordId,\n organizationId: args.organizationId ?? null,\n tenantId: args.tenantId ?? null,\n doc: null,\n trx: args.trx,\n })\n } catch (error) {\n if (args.trx) throw error\n }\n }\n if (existed) {\n await scopeEntityIndexes(\n executor.deleteFrom('entity_indexes' as any) as any,\n args,\n ).execute()\n }\n return { doc: null, existed, wasDeleted, created: false, revived: false }\n }\n\n const payload = {\n entity_type: args.entityType,\n entity_id: String(args.recordId),\n organization_id: args.organizationId ?? null,\n tenant_id: args.tenantId ?? null,\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n index_version: 1,\n updated_at: sql`now()`,\n deleted_at: null,\n }\n\n // Prefer modern upsert keyed by coalesced org id when available; fallback to update-then-insert\n try {\n await executor\n .insertInto('entity_indexes' as any)\n .values({ ...payload, created_at: sql`now()` } as any)\n .onConflict((oc: any) => oc\n .columns(['entity_type', 'entity_id', 'organization_id_coalesced'])\n .doUpdateSet({\n tenant_id: args.tenantId ?? null,\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n index_version: 1,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any))\n .execute()\n } catch {\n // Fallback for schemas without organization_id_coalesced column/index\n const updated = await scopeEntityIndexes(\n executor.updateTable('entity_indexes' as any).set(payload as any) as any,\n args,\n ).executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n if (!updated || Number(updated.numUpdatedRows ?? 0) === 0) {\n try {\n await executor\n .insertInto('entity_indexes' as any)\n .values({ ...payload, created_at: sql`now()` } as any)\n .execute()\n } catch (error) {\n if (args.trx) throw error\n }\n }\n }\n\n const created = !existed\n const revived = existed && wasDeleted\n // The search-token rebuild (DELETE + chunked INSERT) is the heavy tail of indexing.\n // Callers that defer it (the upsert subscriber) run `reindexSearchTokensForRecord`\n // asynchronously after this projection update so write latency stays bounded.\n if (!args.deferSearchTokens) {\n try {\n await reindexSearchTokensForRecord(em, {\n entityType: args.entityType,\n recordId: args.recordId,\n organizationId: args.organizationId ?? null,\n tenantId: args.tenantId ?? null,\n doc,\n searchTokenDoc: args.searchTokenDoc ?? null,\n trx: args.trx,\n })\n } catch (error) {\n if (args.trx) throw error\n }\n }\n return { doc, existed, wasDeleted, created, revived }\n}\n\n/**\n * Rebuilds (or clears, when `doc` is null) the search-token rows for a single record.\n * This is the asynchronous-friendly tail of `upsertIndexRow`: it does not touch the\n * `entity_indexes` projection that list endpoints read, so it can run out-of-band\n * without making query-index reads inconsistent.\n */\nexport async function reindexSearchTokensForRecord(\n em: EntityManager,\n args: {\n entityType: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n doc: Record<string, any> | null\n searchTokenDoc?: Record<string, unknown> | null\n trx?: QueryIndexExecutor\n },\n): Promise<void> {\n const db = (em as any).getKysely()\n if (!args.doc) {\n await deleteSearchTokensForRecord(db, {\n entityType: args.entityType,\n recordId: args.recordId,\n organizationId: args.organizationId ?? null,\n tenantId: args.tenantId ?? null,\n }, { trx: args.trx })\n return\n }\n const tokenDoc = args.searchTokenDoc ?? (() => {\n const encryption = resolveTenantEncryptionService(em as any)\n const dekKeyCache = new Map<string | null, string | null>()\n return decryptIndexDocForSearch(\n args.entityType,\n args.doc,\n { tenantId: args.tenantId ?? null, organizationId: args.organizationId ?? null },\n encryption,\n dekKeyCache,\n )\n })()\n // Rebuilt on the decrypted document: the aggregate stored in `entity_indexes` was\n // composed from the row as it sits at rest, so for every encryption-mapped field it\n // holds ciphertext no user query can ever match (#5625). `resolveSearchConfig` re-parses\n // the environment on every call, so it is resolved once and threaded into both the\n // rebuild and the token writer \u2014 that also makes the aggregate's blocklist filter and\n // the per-field one provably read the same snapshot, as the batch path already does.\n const searchConfig = resolveSearchConfig()\n const searchDoc = rebuildAggregateSearchField(await tokenDoc, {\n entityType: args.entityType,\n config: searchConfig,\n })\n await replaceSearchTokensForRecord(db, {\n entityType: args.entityType,\n recordId: args.recordId,\n organizationId: args.organizationId ?? null,\n tenantId: args.tenantId ?? null,\n doc: searchDoc,\n config: searchConfig,\n }, { trx: args.trx })\n}\n\nexport async function markDeleted(\n em: EntityManager,\n args: { entityType: string; recordId: string; organizationId?: string | null; tenantId?: string | null; trx?: QueryIndexExecutor }\n): Promise<{ wasActive: boolean }> {\n const db = (em as any).getKysely()\n const executor = args.trx ?? db\n const existing = await scopeEntityIndexes(\n executor.selectFrom('entity_indexes' as any).select(['deleted_at' as any]),\n args,\n ).executeTakeFirst() as { deleted_at: Date | null } | undefined\n\n const wasActive = !!existing && existing.deleted_at == null\n\n if (existing) {\n try {\n await deleteSearchTokensForRecord(db, {\n entityType: args.entityType,\n recordId: args.recordId,\n organizationId: args.organizationId ?? null,\n tenantId: args.tenantId ?? null,\n }, { trx: args.trx })\n } catch (error) {\n if (args.trx) throw error\n }\n await scopeEntityIndexes(\n executor.deleteFrom('entity_indexes' as any) as any,\n args,\n ).execute()\n }\n\n return { wasActive }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7929.1.0822b5d5fd",
3
+ "version": "0.10.1-develop.7935.1.cc82ba7697",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -266,16 +266,16 @@
266
266
  "zod": "^4.6.6"
267
267
  },
268
268
  "peerDependencies": {
269
- "@open-mercato/ai-assistant": "0.10.1-develop.7929.1.0822b5d5fd",
270
- "@open-mercato/shared": "0.10.1-develop.7929.1.0822b5d5fd",
271
- "@open-mercato/ui": "0.10.1-develop.7929.1.0822b5d5fd",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7935.1.cc82ba7697",
270
+ "@open-mercato/shared": "0.10.1-develop.7935.1.cc82ba7697",
271
+ "@open-mercato/ui": "0.10.1-develop.7935.1.cc82ba7697",
272
272
  "react": "^19.0.0",
273
273
  "react-dom": "^19.0.0"
274
274
  },
275
275
  "devDependencies": {
276
- "@open-mercato/ai-assistant": "0.10.1-develop.7929.1.0822b5d5fd",
277
- "@open-mercato/shared": "0.10.1-develop.7929.1.0822b5d5fd",
278
- "@open-mercato/ui": "0.10.1-develop.7929.1.0822b5d5fd",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7935.1.cc82ba7697",
277
+ "@open-mercato/shared": "0.10.1-develop.7935.1.cc82ba7697",
278
+ "@open-mercato/ui": "0.10.1-develop.7935.1.cc82ba7697",
279
279
  "@testing-library/dom": "^10.4.1",
280
280
  "@testing-library/jest-dom": "^7.0.1",
281
281
  "@testing-library/react": "^16.3.3",
@@ -23,7 +23,6 @@ export type OcrProcessingDispatch = 'queued' | 'inline_fallback'
23
23
  let activeOcrJobs = 0
24
24
  const ocrWaitQueue: Array<() => void> = []
25
25
  // Jobs scheduled with setImmediate that have not yet entered a slot or the wait queue.
26
- // Counted toward the wait-queue cap so a burst in one event-loop turn cannot pass the check together.
27
26
  let pendingOcrJobs = 0
28
27
 
29
28
  /** Test-only: reset in-process OCR concurrency bookkeeping. */
@@ -176,11 +175,14 @@ export async function requestOcrProcessing(
176
175
  const workerEm = em.fork()
177
176
 
178
177
  const maxWaitQueue = resolveOcrMaxWaitQueue()
179
- if (ocrWaitQueue.length + pendingOcrJobs >= maxWaitQueue) {
178
+ const freeOcrSlots = Math.max(0, resolveOcrMaxConcurrency() - activeOcrJobs)
179
+ const pendingWaiters = Math.max(0, pendingOcrJobs - freeOcrSlots)
180
+ if (ocrWaitQueue.length + pendingWaiters >= maxWaitQueue) {
180
181
  logger.warn('OCR wait queue full; falling back to inline text extraction', {
181
182
  attachmentId: attachment.id,
182
183
  waiting: ocrWaitQueue.length,
183
184
  pending: pendingOcrJobs,
185
+ pendingWaiters,
184
186
  maxWaitQueue,
185
187
  })
186
188
  await persistInlineTextExtractionFallback(workerEm, payload, driver)
@@ -9,7 +9,7 @@ import {
9
9
  requireId,
10
10
  } from '@open-mercato/shared/lib/commands/helpers'
11
11
  import type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
12
- import { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'
12
+ import { CrudHttpError, forbidden, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
13
13
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
14
14
  import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
15
15
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
@@ -192,13 +192,25 @@ const createRoleCommand: CommandHandler<Record<string, unknown>, Role> = {
192
192
  throw new CrudHttpError(400, { error: 'tenantId is required — global roles are not supported' })
193
193
  }
194
194
  const de = (ctx.container.resolve('dataEngine') as DataEngine)
195
- const role = await de.createOrmEntity({
196
- entity: Role,
197
- data: {
198
- name: parsed.name,
199
- tenantId: resolvedTenantId,
200
- },
201
- })
195
+ let role: Role
196
+ try {
197
+ role = await de.createOrmEntity({
198
+ entity: Role,
199
+ data: {
200
+ name: parsed.name,
201
+ tenantId: resolvedTenantId,
202
+ },
203
+ })
204
+ } catch (error) {
205
+ if (!isUniqueViolation(error, 'roles_tenant_id_name_unique')) throw error
206
+ const { translate } = await resolveTranslations()
207
+ const message = translate('auth.roles.errors.nameExists', 'A role with this name already exists')
208
+ throw new CrudHttpError(400, {
209
+ error: message,
210
+ fieldErrors: { name: message },
211
+ details: [{ path: ['name'], message, code: 'duplicate', origin: 'validation' }],
212
+ })
213
+ }
202
214
 
203
215
  await setCustomFieldsIfAny({
204
216
  dataEngine: de,
@@ -614,6 +614,7 @@
614
614
  "auth.resetPassword": "Passwort zurücksetzen",
615
615
  "auth.roles.admin": "Administrator",
616
616
  "auth.roles.employee": "Mitarbeitende",
617
+ "auth.roles.errors.nameExists": "Eine Rolle mit diesem Namen existiert bereits",
617
618
  "auth.roles.flash.created": "Rolle erstellt",
618
619
  "auth.roles.flash.deleted": "Rolle gelöscht",
619
620
  "auth.roles.flash.updated": "Rolle gespeichert",
@@ -614,6 +614,7 @@
614
614
  "auth.resetPassword": "Reset password",
615
615
  "auth.roles.admin": "Admin",
616
616
  "auth.roles.employee": "Employee",
617
+ "auth.roles.errors.nameExists": "A role with this name already exists",
617
618
  "auth.roles.flash.created": "Role created",
618
619
  "auth.roles.flash.deleted": "Role deleted",
619
620
  "auth.roles.flash.updated": "Role saved",
@@ -614,6 +614,7 @@
614
614
  "auth.resetPassword": "Restablecer contraseña",
615
615
  "auth.roles.admin": "Administrador",
616
616
  "auth.roles.employee": "Empleado",
617
+ "auth.roles.errors.nameExists": "Ya existe un rol con este nombre",
617
618
  "auth.roles.flash.created": "Rol creado",
618
619
  "auth.roles.flash.deleted": "Rol eliminado",
619
620
  "auth.roles.flash.updated": "Rol guardado",
@@ -614,6 +614,7 @@
614
614
  "auth.resetPassword": "비밀번호 재설정",
615
615
  "auth.roles.admin": "관리자",
616
616
  "auth.roles.employee": "직원",
617
+ "auth.roles.errors.nameExists": "이 이름의 역할이 이미 존재합니다",
617
618
  "auth.roles.flash.created": "역할이 생성되었습니다",
618
619
  "auth.roles.flash.deleted": "역할이 삭제되었습니다",
619
620
  "auth.roles.flash.updated": "역할이 저장되었습니다",
@@ -614,6 +614,7 @@
614
614
  "auth.resetPassword": "Resetuj hasło",
615
615
  "auth.roles.admin": "Administrator",
616
616
  "auth.roles.employee": "Pracownik",
617
+ "auth.roles.errors.nameExists": "Rola o tej nazwie już istnieje",
617
618
  "auth.roles.flash.created": "Rola utworzona",
618
619
  "auth.roles.flash.deleted": "Rola usunięta",
619
620
  "auth.roles.flash.updated": "Rola zapisana",
@@ -240,7 +240,7 @@
240
240
  "business_rules.rules.form.placeholders.entityType": "Tippen Sie, um Vorschläge zu sehen",
241
241
  "business_rules.rules.form.placeholders.eventType": "Tippen Sie, um Vorschläge zu sehen",
242
242
  "business_rules.rules.form.placeholders.ruleCategory": "z.B. Preisgestaltung, Workflow, Datenqualität",
243
- "business_rules.rules.form.placeholders.ruleId": "AUTO_GENERATED_ID",
243
+ "business_rules.rules.form.placeholders.ruleId": "ORDER_APPROVAL_CHECK",
244
244
  "business_rules.rules.form.placeholders.ruleName": "Geben Sie einen beschreibenden Regelnamen ein",
245
245
  "business_rules.rules.form.priority": "Priorität",
246
246
  "business_rules.rules.form.ruleCategory": "Kategorie",
@@ -240,7 +240,7 @@
240
240
  "business_rules.rules.form.placeholders.entityType": "Start typing to see suggestions",
241
241
  "business_rules.rules.form.placeholders.eventType": "Start typing to see suggestions",
242
242
  "business_rules.rules.form.placeholders.ruleCategory": "e.g., Pricing, Workflow, Data Quality",
243
- "business_rules.rules.form.placeholders.ruleId": "AUTO_GENERATED_ID",
243
+ "business_rules.rules.form.placeholders.ruleId": "ORDER_APPROVAL_CHECK",
244
244
  "business_rules.rules.form.placeholders.ruleName": "Enter a descriptive rule name",
245
245
  "business_rules.rules.form.priority": "Priority",
246
246
  "business_rules.rules.form.ruleCategory": "Category",
@@ -240,7 +240,7 @@
240
240
  "business_rules.rules.form.placeholders.entityType": "Empiece a escribir para ver sugerencias",
241
241
  "business_rules.rules.form.placeholders.eventType": "Empiece a escribir para ver sugerencias",
242
242
  "business_rules.rules.form.placeholders.ruleCategory": "ej. Precios, Flujo de trabajo, Calidad de datos",
243
- "business_rules.rules.form.placeholders.ruleId": "ID_AUTO_GENERADO",
243
+ "business_rules.rules.form.placeholders.ruleId": "ORDER_APPROVAL_CHECK",
244
244
  "business_rules.rules.form.placeholders.ruleName": "Ingrese un nombre descriptivo para la regla",
245
245
  "business_rules.rules.form.priority": "Prioridad",
246
246
  "business_rules.rules.form.ruleCategory": "Categoría",
@@ -240,7 +240,7 @@
240
240
  "business_rules.rules.form.placeholders.entityType": "입력을 시작하면 제안이 표시됩니다",
241
241
  "business_rules.rules.form.placeholders.eventType": "입력을 시작하면 제안이 표시됩니다",
242
242
  "business_rules.rules.form.placeholders.ruleCategory": "예: 가격 책정, 워크플로, 데이터 품질",
243
- "business_rules.rules.form.placeholders.ruleId": "AUTO_GENERATED_ID",
243
+ "business_rules.rules.form.placeholders.ruleId": "ORDER_APPROVAL_CHECK",
244
244
  "business_rules.rules.form.placeholders.ruleName": "설명적인 규칙 이름을 입력하세요",
245
245
  "business_rules.rules.form.priority": "우선순위",
246
246
  "business_rules.rules.form.ruleCategory": "카테고리",
@@ -240,7 +240,7 @@
240
240
  "business_rules.rules.form.placeholders.entityType": "Zacznij pisać, aby zobaczyć podpowiedzi",
241
241
  "business_rules.rules.form.placeholders.eventType": "Zacznij pisać, aby zobaczyć podpowiedzi",
242
242
  "business_rules.rules.form.placeholders.ruleCategory": "np. Cenowanie, Przepływ pracy, Jakość danych",
243
- "business_rules.rules.form.placeholders.ruleId": "AUTO_GENERATED_ID",
243
+ "business_rules.rules.form.placeholders.ruleId": "ORDER_APPROVAL_CHECK",
244
244
  "business_rules.rules.form.placeholders.ruleName": "Wprowadź opisową nazwę reguły",
245
245
  "business_rules.rules.form.priority": "Priorytet",
246
246
  "business_rules.rules.form.ruleCategory": "Kategoria",
@@ -200,7 +200,8 @@ export async function setRecordCustomFields(
200
200
  // the caller's organization (#6468). Collapse it to one value so the scalar branch
201
201
  // below reconciles the duplicates away.
202
202
  const collapseToSingle = Array.isArray(raw) && Boolean(def) && (def as any).configJson?.multi !== true
203
- const isArray = Array.isArray(raw) && !collapseToSingle
203
+ const isArray = (Array.isArray(raw) && !collapseToSingle)
204
+ || (raw === null && def?.configJson?.multi === true)
204
205
  // When array (multi-value): replace all existing rows for the key. Delete
205
206
  // first, then create replacements, all inside the transaction opened above.
206
207
  // Creating rows before a native delete can auto-flush and delete the new
@@ -208,7 +209,7 @@ export async function setRecordCustomFields(
208
209
  // observed to commit an empty set under MikroORM v7. The explicit order keeps
209
210
  // the replacement atomic without letting old-row cleanup target new rows.
210
211
  if (isArray) {
211
- const arr = raw as Primitive[]
212
+ const arr = Array.isArray(raw) ? raw.filter((value) => value != null) : []
212
213
  // Reconciling delete (see tenantScopeFilter/reconcileScopeFilter): pinning
213
214
  // organizationId here left the rows written under the record's PREVIOUS
214
215
  // organization alive next to the replacements, and the Query Engine's value join
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto'
1
2
  import { resolveQueryIndexSourceTableMetadata } from './subscriber-scope'
2
3
  import type { EntityManager } from '@mikro-orm/postgresql'
3
4
  import { resolveEntityTableName as resolveFallbackEntityTableName } from '@open-mercato/shared/lib/query/engine'
@@ -11,6 +12,8 @@ import {
11
12
  } from '@open-mercato/shared/lib/crud/custom-field-definition-index'
12
13
  import { type Kysely, type Transaction, sql } from 'kysely'
13
14
  import { createLogger } from '@open-mercato/shared/lib/logger'
15
+ import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
16
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
14
17
  import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
15
18
  import { replaceSearchTokensForRecord, deleteSearchTokensForRecord } from './search-tokens'
16
19
  import { attachAggregateSearchField, rebuildAggregateSearchField } from './document'
@@ -22,6 +25,17 @@ function resolveEntityTableName(em: EntityManager, entity: string): string {
22
25
 
23
26
  const logger = createLogger('query_index').child({ component: 'indexer' })
24
27
 
28
+ function reportTranslationsFailure(
29
+ error: unknown,
30
+ code: 'query_index.translations_read_failed' | 'query_index.translations_log_failed',
31
+ ): void {
32
+ try {
33
+ getTelemetryRuntime()?.reportError(error, { module: 'query_index', code })
34
+ } catch (telemetryError) {
35
+ logger.warn('Failed to report index translations error', { err: telemetryError })
36
+ }
37
+ }
38
+
25
39
  type QueryIndexExecutor = Kysely<any> | Transaction<any>
26
40
 
27
41
  type BuildDocParams = {
@@ -182,6 +196,8 @@ export async function buildIndexDoc(em: EntityManager, params: BuildDocParams):
182
196
  }
183
197
 
184
198
  // Attach translations under flat keys 'l10n:{locale}:{field}'
199
+ const translationsSavepoint = db.isTransaction ? `om_translations_${randomUUID()}` : null
200
+ if (translationsSavepoint) await sql`SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db)
185
201
  try {
186
202
  const translationRow = await db
187
203
  .selectFrom('entity_translations' as any)
@@ -202,7 +218,34 @@ export async function buildIndexDoc(em: EntityManager, params: BuildDocParams):
202
218
  }
203
219
  }
204
220
  }
205
- } catch {}
221
+ } catch (translationsError) {
222
+ if (translationsSavepoint) {
223
+ await sql`ROLLBACK TO SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db)
224
+ }
225
+ reportTranslationsFailure(translationsError, 'query_index.translations_read_failed')
226
+ await recordIndexerError(
227
+ { db },
228
+ {
229
+ source: 'query_index',
230
+ handler: 'query_index:build-index-doc:translations',
231
+ error: translationsError,
232
+ entityType: params.entityType,
233
+ recordId: params.recordId,
234
+ tenantId: params.tenantId ?? null,
235
+ organizationId: params.organizationId ?? null,
236
+ },
237
+ ).catch((loggingError: unknown) => reportTranslationsFailure(loggingError, 'query_index.translations_log_failed'))
238
+ } finally {
239
+ if (translationsSavepoint) {
240
+ try {
241
+ await sql`RELEASE SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db)
242
+ } catch (loggingError) {
243
+ await sql`ROLLBACK TO SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db)
244
+ await sql`RELEASE SAVEPOINT ${sql.id(translationsSavepoint)}`.execute(db)
245
+ reportTranslationsFailure(loggingError, 'query_index.translations_log_failed')
246
+ }
247
+ }
248
+ }
206
249
 
207
250
  await applyIndexDocEnrichers(db, params.entityType, [{
208
251
  recordId: String(params.recordId),