@open-mercato/core 0.6.7-develop.6653.1.b5bc7194a0 → 0.6.7-develop.6661.1.0043ed1d03

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/business_rules/lib/value-resolver.js +7 -0
  3. package/dist/modules/business_rules/lib/value-resolver.js.map +2 -2
  4. package/dist/modules/communication_channels/lib/pg-errors.js +1 -7
  5. package/dist/modules/communication_channels/lib/pg-errors.js.map +2 -2
  6. package/dist/modules/currencies/commands/currencies.js +22 -8
  7. package/dist/modules/currencies/commands/currencies.js.map +2 -2
  8. package/dist/modules/currencies/commands/exchange-rates.js +22 -8
  9. package/dist/modules/currencies/commands/exchange-rates.js.map +2 -2
  10. package/dist/modules/currencies/commands/scope.js +22 -0
  11. package/dist/modules/currencies/commands/scope.js.map +7 -0
  12. package/dist/modules/customers/components/detail/ActivitiesSection.js +5 -4
  13. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
  14. package/dist/modules/customers/components/detail/ActivityHistorySection.js +5 -4
  15. package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
  16. package/dist/modules/feature_toggles/api/global/route.js +6 -0
  17. package/dist/modules/feature_toggles/api/global/route.js.map +2 -2
  18. package/dist/modules/feature_toggles/commands/global.js +9 -9
  19. package/dist/modules/feature_toggles/commands/global.js.map +2 -2
  20. package/dist/modules/query_index/api/openapi.js +2 -2
  21. package/dist/modules/query_index/api/openapi.js.map +2 -2
  22. package/dist/modules/query_index/api/status.js +6 -4
  23. package/dist/modules/query_index/api/status.js.map +2 -2
  24. package/dist/modules/query_index/cli.js +31 -28
  25. package/dist/modules/query_index/cli.js.map +2 -2
  26. package/dist/modules/query_index/components/QueryIndexesTable.js +4 -1
  27. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  28. package/dist/modules/query_index/lib/batch.js +163 -44
  29. package/dist/modules/query_index/lib/batch.js.map +3 -3
  30. package/dist/modules/query_index/lib/jobs.js +3 -2
  31. package/dist/modules/query_index/lib/jobs.js.map +2 -2
  32. package/dist/modules/query_index/lib/reindexer.js +50 -16
  33. package/dist/modules/query_index/lib/reindexer.js.map +2 -2
  34. package/dist/modules/query_index/lib/stale.js +3 -0
  35. package/dist/modules/query_index/lib/stale.js.map +2 -2
  36. package/dist/modules/query_index/lib/subscriber-scope.js +63 -10
  37. package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
  38. package/dist/modules/query_index/subscribers/delete_one.js +20 -6
  39. package/dist/modules/query_index/subscribers/delete_one.js.map +2 -2
  40. package/dist/modules/query_index/subscribers/upsert_one.js +8 -3
  41. package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
  42. package/dist/modules/workflows/api/definitions/[id]/customize/route.js +2 -0
  43. package/dist/modules/workflows/api/definitions/[id]/customize/route.js.map +2 -2
  44. package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js +4 -0
  45. package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js.map +2 -2
  46. package/package.json +7 -7
  47. package/src/modules/business_rules/lib/value-resolver.ts +16 -0
  48. package/src/modules/communication_channels/lib/pg-errors.ts +3 -10
  49. package/src/modules/currencies/commands/currencies.ts +26 -8
  50. package/src/modules/currencies/commands/exchange-rates.ts +26 -8
  51. package/src/modules/currencies/commands/scope.ts +32 -0
  52. package/src/modules/customers/components/detail/ActivitiesSection.tsx +10 -6
  53. package/src/modules/customers/components/detail/ActivityHistorySection.tsx +6 -3
  54. package/src/modules/feature_toggles/api/global/route.ts +7 -1
  55. package/src/modules/feature_toggles/commands/global.ts +9 -12
  56. package/src/modules/query_index/api/openapi.ts +2 -2
  57. package/src/modules/query_index/api/status.ts +14 -5
  58. package/src/modules/query_index/cli.ts +36 -28
  59. package/src/modules/query_index/components/QueryIndexesTable.tsx +7 -4
  60. package/src/modules/query_index/i18n/de.json +2 -0
  61. package/src/modules/query_index/i18n/en.json +2 -0
  62. package/src/modules/query_index/i18n/es.json +2 -0
  63. package/src/modules/query_index/i18n/pl.json +2 -0
  64. package/src/modules/query_index/lib/batch.ts +244 -67
  65. package/src/modules/query_index/lib/jobs.ts +11 -0
  66. package/src/modules/query_index/lib/reindexer.ts +69 -16
  67. package/src/modules/query_index/lib/stale.ts +10 -0
  68. package/src/modules/query_index/lib/subscriber-scope.ts +98 -14
  69. package/src/modules/query_index/subscribers/delete_one.ts +21 -10
  70. package/src/modules/query_index/subscribers/upsert_one.ts +8 -3
  71. package/src/modules/workflows/AGENTS.md +22 -0
  72. package/src/modules/workflows/api/definitions/[id]/customize/route.ts +9 -0
  73. package/src/modules/workflows/api/definitions/[id]/reset-to-code/route.ts +7 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/lib/reindexer.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { resolveRegisteredEntityTableName } 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 { upsertIndexBatch, type AnyRow } from './batch'\nimport { refreshCoverageSnapshot, writeCoverageCounts, applyCoverageAdjustments } from './coverage'\nimport { prepareJob, updateJobProgress, finalizeJob, type JobScope } from './jobs'\nimport { purgeOrphans } from './stale'\nimport type { VectorIndexService } from '@open-mercato/search/vector'\nimport { isSearchDebugEnabled } from './search-tokens'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('query_index').child({ component: 'reindexer' })\n\nexport type ReindexJobOptions = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n force?: boolean\n batchSize?: number\n emitVectorizeEvents?: boolean\n eventBus?: {\n emitEvent(event: string, payload: any, options?: any): Promise<void>\n }\n partitionCount?: number\n partitionIndex?: number\n resetCoverage?: boolean\n onProgress?: (info: { processed: number; total: number; chunkSize: number }) => void\n vectorService?: VectorIndexService | null\n}\n\nexport type ReindexJobResult = {\n processed: number\n total: number\n tenantScopes: Array<string | null>\n scopes: Array<{ tenantId: string | null; organizationId: string | null }>\n}\n\nexport const DEFAULT_REINDEX_PARTITIONS = 5\nconst DEFAULT_BATCH_SIZE = 500\nconst deriveOrgFromId = new Set<string>(['directory:organization'])\nconst COVERAGE_REFRESH_THROTTLE_MS = 5 * 60 * 1000\nconst lastCoverageReset = new Map<string, number>()\n\nconst REINDEX_DECRYPT_DEBUG_KEYS = ['display_name', 'first_name', 'last_name', 'brand_name', 'legal_name', 'primary_email', 'primary_phone'] as const\n\nexport type ReindexDecryptDebugPayload = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n keys: string[]\n}\n\nexport function buildReindexDecryptDebugPayload(\n entityType: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n): ReindexDecryptDebugPayload {\n const presentKeys: string[] = []\n for (const key of REINDEX_DECRYPT_DEBUG_KEYS) {\n const value = doc[key]\n if (key in doc && value != null && value !== '') presentKeys.push(key)\n }\n return {\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n keys: presentKeys,\n }\n}\n\nasync function cleanupLegacyJobScopes(\n db: Kysely<any>,\n options: {\n entityType: string\n organizationId: string | null\n tenantId: string | null\n activePartitionCount: number | null\n },\n): Promise<void> {\n await db\n .deleteFrom('entity_index_jobs' as any)\n .where('entity_type' as any, '=', options.entityType)\n .where(sql<boolean>`organization_id is not distinct from ${options.organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${options.tenantId}`)\n .where(sql<boolean>`partition_count is distinct from ${options.activePartitionCount}`)\n .execute()\n}\n\nfunction toNumber(value: unknown): number {\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n}\n\nasync function getColumnSet(db: Kysely<any>, tableName: string): Promise<Set<string>> {\n try {\n const rows = await db\n .selectFrom('information_schema.columns' as any)\n .select(['column_name' as any])\n .where(sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, '=', tableName)\n .execute() as Array<{ column_name: string }>\n return new Set(rows.map((row) => String(row.column_name).toLowerCase()))\n } catch {\n return new Set<string>()\n }\n}\n\nexport async function reindexEntity(\n em: EntityManager,\n options: ReindexJobOptions,\n): Promise<ReindexJobResult> {\n const entityType = String(options?.entityType || '')\n if (!entityType) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const tenantIdInput = options?.tenantId\n const tenantId = tenantIdInput === 'undefined' ? undefined : tenantIdInput\n const organizationIdInput = options?.organizationId\n const organizationId = organizationIdInput === 'undefined' ? undefined : organizationIdInput\n const force = options?.force === true\n const batchSize = Number.isFinite(options?.batchSize) && options!.batchSize! > 0\n ? Math.max(1, Math.trunc(options!.batchSize!))\n : DEFAULT_BATCH_SIZE\n const emitVectorize = options?.emitVectorizeEvents === true\n const eventBus = options?.eventBus\n const vectorService = options?.vectorService ?? null\n const partitionCountRaw = Number.isFinite(options?.partitionCount)\n ? Math.max(1, Math.trunc(options!.partitionCount!))\n : 1\n const usingPartitions = partitionCountRaw > 1\n const partitionIndexRaw = Number.isFinite(options?.partitionIndex)\n ? Math.max(0, Math.trunc(options!.partitionIndex!))\n : 0\n const partitionIndex = usingPartitions\n ? Math.min(partitionIndexRaw, partitionCountRaw - 1)\n : null\n const resetCoverage = options?.resetCoverage ?? (!usingPartitions || partitionIndex === 0)\n\n const db = (em as any).getKysely() as Kysely<any>\n // Resolve the source table strictly via registered MikroORM metadata. We must\n // never fall back to a pluralized guess derived from the caller-supplied id\n // here: doing so would let a principal with `query_index.reindex` point the\n // reindexer at arbitrary tables (e.g. `auth_users`, `users`) and read their\n // rows into the index, bypassing tenant scoping and entity-level encryption.\n const table = resolveRegisteredEntityTableName(em, entityType)\n if (!table || entityType === 'query_index:search_token' || table === 'search_tokens') {\n if (!table) {\n logger.warn('Refusing to reindex unregistered entity type', {\n entityType,\n })\n }\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const columns = await getColumnSet(db, table)\n const hasOrgCol = columns.has('organization_id')\n const hasTenantCol = columns.has('tenant_id')\n const hasDeletedCol = columns.has('deleted_at')\n\n const jobScope: JobScope = {\n entityType,\n organizationId: organizationId ?? null,\n tenantId: tenantId ?? null,\n partitionIndex,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n }\n\n if (!force) {\n const activeJob = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['id' as any])\n .where('entity_type' as any, '=', entityType)\n .where('finished_at' as any, 'is', null as any)\n .where(sql<boolean>`organization_id is not distinct from ${null}`)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .executeTakeFirst()\n if (activeJob) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n }\n\n if (resetCoverage) {\n await cleanupLegacyJobScopes(db, {\n entityType,\n organizationId: jobScope.organizationId ?? null,\n tenantId: jobScope.tenantId ?? null,\n activePartitionCount: jobScope.partitionCount ?? null,\n })\n }\n\n const scopeKey = (tenantValue: string | null, orgValue: string | null) => `${tenantValue ?? '__null__'}|${orgValue ?? '__null__'}`\n\n const applyBaseWhere = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n if (hasDeletedCol) chain = chain.where('b.deleted_at' as any, 'is', null as any)\n if (tenantId !== undefined && hasTenantCol) {\n chain = tenantId === null\n ? chain.where('b.tenant_id' as any, 'is', null as any)\n : chain.where('b.tenant_id' as any, '=', tenantId)\n }\n if (organizationId !== undefined && hasOrgCol) {\n chain = organizationId === null\n ? chain.where('b.organization_id' as any, 'is', null as any)\n : chain.where('b.organization_id' as any, '=', organizationId)\n }\n if (usingPartitions && partitionIndex !== null) {\n chain = chain.where(sql<boolean>`mod(abs(hashtext(b.id::text)), ${partitionCountRaw}) = ${partitionIndex}`)\n }\n return chain\n }\n\n type ScopeStats = { tenantId: string | null; organizationId: string | null; count: number }\n const baseCounts = new Map<string, ScopeStats>()\n const registerBaseCount = (tenantValue: string | null, orgValue: string | null, count: number) => {\n const key = scopeKey(tenantValue, orgValue)\n baseCounts.set(key, { tenantId: tenantValue, organizationId: orgValue, count })\n }\n\n const groupByTenant = hasTenantCol && tenantId === undefined\n const groupByOrg = hasOrgCol && organizationId === undefined\n\n if (groupByTenant || groupByOrg) {\n let groupQuery = applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n )\n if (groupByTenant) {\n groupQuery = groupQuery.select('b.tenant_id as tenant_id' as any).groupBy('b.tenant_id' as any)\n }\n if (groupByOrg) {\n groupQuery = groupQuery.select('b.organization_id as organization_id' as any).groupBy('b.organization_id' as any)\n }\n const rows = await groupQuery.execute() as Array<Record<string, unknown>>\n for (const row of rows) {\n const bucketTenant = groupByTenant\n ? ((row as any)?.tenant_id ?? null)\n : (tenantId === undefined ? null : tenantId ?? null)\n const bucketOrg = groupByOrg\n ? ((row as any)?.organization_id ?? null)\n : (organizationId === undefined ? null : organizationId ?? null)\n registerBaseCount(bucketTenant, bucketOrg, toNumber((row as any)?.count))\n }\n } else {\n const row = await applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n ).executeTakeFirst() as { count: unknown } | undefined\n const bucketTenant = tenantId === undefined ? null : tenantId ?? null\n const bucketOrg = organizationId === undefined ? null : organizationId ?? null\n registerBaseCount(bucketTenant, bucketOrg, toNumber(row?.count))\n }\n\n const total = Array.from(baseCounts.values()).reduce((acc, value) => acc + (Number.isFinite(value.count) ? value.count : 0), 0)\n await prepareJob(db, jobScope, 'reindexing', { totalCount: total })\n const jobRow = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['started_at' as any])\n .where('entity_type' as any, '=', entityType)\n .where('organization_id' as any, 'is', null as any)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .orderBy('started_at' as any, 'desc')\n .executeTakeFirst() as { started_at: Date | string } | undefined\n const jobStartedAt = jobRow?.started_at ? new Date(jobRow.started_at) : new Date()\n const deriveOrg = deriveOrgFromId.has(entityType)\n ? (row: AnyRow) => String(row.id)\n : undefined\n\n const scopeOverrides: { tenantId?: string; orgId?: string } = {}\n if (tenantId !== undefined && tenantId !== null) {\n scopeOverrides.tenantId = String(tenantId)\n }\n if (organizationId !== undefined && organizationId !== null) {\n scopeOverrides.orgId = String(organizationId)\n }\n\n const scopeEntries = Array.from(baseCounts.values()).map((entry) => ({\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n }))\n const tenantScopes = Array.from(\n new Set(scopeEntries.map((entry) => entry.tenantId ?? null)),\n )\n\n let processed = 0\n let lastId: string | null = null\n\n options?.onProgress?.({ processed, total, chunkSize: 0 })\n\n if (resetCoverage) {\n if (force) {\n try {\n let purgeQuery = db\n .deleteFrom('entity_indexes' as any)\n .where('entity_type' as any, '=', entityType)\n if (tenantId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n }\n if (organizationId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`organization_id is not distinct from ${organizationId ?? null}`)\n }\n await purgeQuery.execute()\n } catch (error) {\n logger.warn('Failed to purge index rows before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n }\n\n if (emitVectorize && eventBus) {\n if (tenantId !== undefined) {\n const payload: Record<string, unknown> = {\n entityType,\n tenantId: tenantId ?? null,\n }\n if (organizationId !== undefined) payload.organizationId = organizationId ?? null\n try {\n await eventBus.emitEvent('query_index.vectorize_purge', payload)\n } catch (err) {\n logger.warn('Failed to queue vector purge before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: err instanceof Error ? err.message : err,\n })\n }\n } else {\n logger.warn('Skipping vector purge for force reindex without tenant scope', {\n entityType,\n })\n }\n }\n }\n\n const nowTs = Date.now()\n for (const scope of baseCounts.values()) {\n const key = `${entityType}|${scopeKey(scope.tenantId, scope.organizationId)}`\n const last = lastCoverageReset.get(key) ?? 0\n if (force || nowTs - last >= COVERAGE_REFRESH_THROTTLE_MS) {\n await writeCoverageCounts(em, {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n }, {\n baseCount: scope.count,\n indexedCount: 0,\n vectorCount: emitVectorize ? 0 : undefined,\n })\n lastCoverageReset.set(key, nowTs)\n }\n }\n }\n\n try {\n while (true) {\n let query = applyBaseWhere(\n db\n .selectFrom(`${table} as b` as any)\n .selectAll('b' as any)\n .orderBy('b.id' as any, 'asc')\n .limit(batchSize),\n )\n if (lastId !== null) {\n query = query.where('b.id' as any, '>', lastId)\n }\n const rows = await query.execute() as AnyRow[]\n if (!rows.length) break\n\n const encryption = resolveTenantEncryptionService(em as any)\n const dekKeyCache = new Map<string | null, string | null>()\n const encryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n return await encryptIndexDocForStorage(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n )\n }\n const decryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n const result = await decryptIndexDocForSearch(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n dekKeyCache,\n )\n if (isSearchDebugEnabled()) {\n logger.debug('Reindex decrypt', buildReindexDecryptDebugPayload(targetEntity, result as Record<string, unknown>, scope))\n }\n return result\n }\n\n await upsertIndexBatch(db, entityType, rows, scopeOverrides, { deriveOrganizationId: deriveOrg, encryptDoc, decryptDoc })\n\n const coverageDeltas = new Map<string, { tenantId: string | null; organizationId: string | null; delta: number }>()\n for (const row of rows) {\n const scopeTenant = tenantId !== undefined\n ? tenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n const scopeOrg = organizationId !== undefined\n ? organizationId ?? null\n : (hasOrgCol ? ((row as AnyRow).organization_id ?? null) : (deriveOrg ? deriveOrg(row) ?? null : null))\n const key = scopeKey(scopeTenant ?? null, scopeOrg ?? null)\n const existingDelta = coverageDeltas.get(key)\n if (existingDelta) existingDelta.delta += 1\n else coverageDeltas.set(key, {\n tenantId: scopeTenant ?? null,\n organizationId: scopeOrg ?? null,\n delta: 1,\n })\n }\n if (coverageDeltas.size > 0) {\n await applyCoverageAdjustments(\n em,\n Array.from(coverageDeltas.values()).map((entry) => ({\n entityType,\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n withDeleted: false,\n deltaBase: 0,\n deltaIndex: entry.delta,\n })),\n )\n }\n\n if (emitVectorize && eventBus) {\n await Promise.all(\n rows.map((row) => {\n const scopeOrg = organizationId !== undefined\n ? organizationId ?? null\n : hasOrgCol\n ? ((row as AnyRow).organization_id ?? null)\n : (deriveOrg ? deriveOrg(row) ?? null : null)\n const scopeTenant = tenantId !== undefined\n ? tenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n return eventBus\n .emitEvent('query_index.vectorize_one', {\n entityType,\n recordId: String(row.id),\n organizationId: scopeOrg,\n tenantId: scopeTenant,\n })\n .catch(() => undefined)\n }),\n )\n }\n\n processed += rows.length\n lastId = String(rows[rows.length - 1]!.id)\n options?.onProgress?.({ processed, total, chunkSize: rows.length })\n await updateJobProgress(db, jobScope, rows.length)\n }\n\n await purgeOrphans(db, {\n entityType,\n tenantId,\n organizationId,\n partitionIndex: usingPartitions ? partitionIndex : null,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n startedAt: jobStartedAt,\n })\n\n if (force && vectorService && (!usingPartitions || partitionIndex === null)) {\n try {\n await vectorService.removeOrphans({\n entityId: entityType,\n tenantId,\n organizationId,\n olderThan: jobStartedAt,\n })\n } catch (error) {\n logger.warn('Failed to prune vector orphans after reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n for (const scope of scopeEntries) {\n await refreshCoverageSnapshot(\n em,\n {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n },\n )\n }\n } finally {\n await finalizeJob(db, jobScope)\n }\n\n return {\n processed,\n total,\n scopes: scopeEntries,\n tenantScopes,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { resolveRegisteredEntityTableName } 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 upsertIndexBatch,\n assertIndexBatchWritesLanded,\n createEmptyUpsertIndexBatchResult,\n mergeUpsertIndexBatchResults,\n QueryIndexBatchWriteError,\n type AnyRow,\n} from './batch'\nimport { refreshCoverageSnapshot, writeCoverageCounts, applyCoverageAdjustments } from './coverage'\nimport { prepareJob, updateJobProgress, finalizeJob, type JobScope } from './jobs'\nimport { purgeOrphans } from './stale'\nimport type { VectorIndexService } from '@open-mercato/search/vector'\nimport { isSearchDebugEnabled } from './search-tokens'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('query_index').child({ component: 'reindexer' })\n\nexport type ReindexJobOptions = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n force?: boolean\n batchSize?: number\n emitVectorizeEvents?: boolean\n eventBus?: {\n emitEvent(event: string, payload: any, options?: any): Promise<void>\n }\n partitionCount?: number\n partitionIndex?: number\n resetCoverage?: boolean\n onProgress?: (info: { processed: number; total: number; chunkSize: number }) => void\n vectorService?: VectorIndexService | null\n}\n\nexport type ReindexJobResult = {\n processed: number\n total: number\n tenantScopes: Array<string | null>\n scopes: Array<{ tenantId: string | null; organizationId: string | null }>\n}\n\nexport const DEFAULT_REINDEX_PARTITIONS = 5\nconst DEFAULT_BATCH_SIZE = 500\n/**\n * Above this many failed records the purge exclusion list stops being a sane query, so\n * the purge is skipped entirely instead. Failing closed keeps stale rows; the alternative\n * deletes index entries the run failed to rebuild.\n */\nconst MAX_PURGE_EXCLUSIONS = 1000\nconst deriveOrgFromId = new Set<string>(['directory:organization'])\nconst COVERAGE_REFRESH_THROTTLE_MS = 5 * 60 * 1000\nconst lastCoverageReset = new Map<string, number>()\n\nconst REINDEX_DECRYPT_DEBUG_KEYS = ['display_name', 'first_name', 'last_name', 'brand_name', 'legal_name', 'primary_email', 'primary_phone'] as const\n\nexport type ReindexDecryptDebugPayload = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n keys: string[]\n}\n\nexport function buildReindexDecryptDebugPayload(\n entityType: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n): ReindexDecryptDebugPayload {\n const presentKeys: string[] = []\n for (const key of REINDEX_DECRYPT_DEBUG_KEYS) {\n const value = doc[key]\n if (key in doc && value != null && value !== '') presentKeys.push(key)\n }\n return {\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n keys: presentKeys,\n }\n}\n\nasync function cleanupLegacyJobScopes(\n db: Kysely<any>,\n options: {\n entityType: string\n organizationId: string | null\n tenantId: string | null\n activePartitionCount: number | null\n },\n): Promise<void> {\n await db\n .deleteFrom('entity_index_jobs' as any)\n .where('entity_type' as any, '=', options.entityType)\n .where(sql<boolean>`organization_id is not distinct from ${options.organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${options.tenantId}`)\n .where(sql<boolean>`partition_count is distinct from ${options.activePartitionCount}`)\n .execute()\n}\n\nfunction toNumber(value: unknown): number {\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n}\n\nasync function getColumnSet(db: Kysely<any>, tableName: string): Promise<Set<string>> {\n try {\n const rows = await db\n .selectFrom('information_schema.columns' as any)\n .select(['column_name' as any])\n .where(sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, '=', tableName)\n .execute() as Array<{ column_name: string }>\n return new Set(rows.map((row) => String(row.column_name).toLowerCase()))\n } catch {\n return new Set<string>()\n }\n}\n\nexport async function reindexEntity(\n em: EntityManager,\n options: ReindexJobOptions,\n): Promise<ReindexJobResult> {\n const entityType = String(options?.entityType || '')\n if (!entityType) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const tenantIdInput = options?.tenantId\n const tenantId = tenantIdInput === 'undefined' ? undefined : tenantIdInput\n const organizationIdInput = options?.organizationId\n const organizationId = organizationIdInput === 'undefined' ? undefined : organizationIdInput\n const force = options?.force === true\n const batchSize = Number.isFinite(options?.batchSize) && options!.batchSize! > 0\n ? Math.max(1, Math.trunc(options!.batchSize!))\n : DEFAULT_BATCH_SIZE\n const emitVectorize = options?.emitVectorizeEvents === true\n const eventBus = options?.eventBus\n const vectorService = options?.vectorService ?? null\n const partitionCountRaw = Number.isFinite(options?.partitionCount)\n ? Math.max(1, Math.trunc(options!.partitionCount!))\n : 1\n const usingPartitions = partitionCountRaw > 1\n const partitionIndexRaw = Number.isFinite(options?.partitionIndex)\n ? Math.max(0, Math.trunc(options!.partitionIndex!))\n : 0\n const partitionIndex = usingPartitions\n ? Math.min(partitionIndexRaw, partitionCountRaw - 1)\n : null\n const resetCoverage = options?.resetCoverage ?? (!usingPartitions || partitionIndex === 0)\n\n const db = (em as any).getKysely() as Kysely<any>\n // Resolve the source table strictly via registered MikroORM metadata. We must\n // never fall back to a pluralized guess derived from the caller-supplied id\n // here: doing so would let a principal with `query_index.reindex` point the\n // reindexer at arbitrary tables (e.g. `auth_users`, `users`) and read their\n // rows into the index, bypassing tenant scoping and entity-level encryption.\n const table = resolveRegisteredEntityTableName(em, entityType)\n if (!table || entityType === 'query_index:search_token' || table === 'search_tokens') {\n if (!table) {\n logger.warn('Refusing to reindex unregistered entity type', {\n entityType,\n })\n }\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const columns = await getColumnSet(db, table)\n const hasOrgCol = columns.has('organization_id')\n const hasTenantCol = columns.has('tenant_id')\n const hasDeletedCol = columns.has('deleted_at')\n\n const jobScope: JobScope = {\n entityType,\n organizationId: organizationId ?? null,\n tenantId: tenantId ?? null,\n partitionIndex,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n }\n\n if (!force) {\n const activeJob = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['id' as any])\n .where('entity_type' as any, '=', entityType)\n .where('finished_at' as any, 'is', null as any)\n .where(sql<boolean>`organization_id is not distinct from ${null}`)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .executeTakeFirst()\n if (activeJob) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n }\n\n if (resetCoverage) {\n await cleanupLegacyJobScopes(db, {\n entityType,\n organizationId: jobScope.organizationId ?? null,\n tenantId: jobScope.tenantId ?? null,\n activePartitionCount: jobScope.partitionCount ?? null,\n })\n }\n\n const scopeKey = (tenantValue: string | null, orgValue: string | null) => `${tenantValue ?? '__null__'}|${orgValue ?? '__null__'}`\n\n const applyBaseWhere = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n if (hasDeletedCol) chain = chain.where('b.deleted_at' as any, 'is', null as any)\n if (tenantId !== undefined && hasTenantCol) {\n chain = tenantId === null\n ? chain.where('b.tenant_id' as any, 'is', null as any)\n : chain.where('b.tenant_id' as any, '=', tenantId)\n }\n if (organizationId !== undefined && hasOrgCol) {\n chain = organizationId === null\n ? chain.where('b.organization_id' as any, 'is', null as any)\n : chain.where('b.organization_id' as any, '=', organizationId)\n }\n if (usingPartitions && partitionIndex !== null) {\n chain = chain.where(sql<boolean>`mod(abs(hashtext(b.id::text)), ${partitionCountRaw}) = ${partitionIndex}`)\n }\n return chain\n }\n\n type ScopeStats = { tenantId: string | null; organizationId: string | null; count: number }\n const baseCounts = new Map<string, ScopeStats>()\n const registerBaseCount = (tenantValue: string | null, orgValue: string | null, count: number) => {\n const key = scopeKey(tenantValue, orgValue)\n baseCounts.set(key, { tenantId: tenantValue, organizationId: orgValue, count })\n }\n\n const groupByTenant = hasTenantCol && tenantId === undefined\n const groupByOrg = hasOrgCol && organizationId === undefined\n\n if (groupByTenant || groupByOrg) {\n let groupQuery = applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n )\n if (groupByTenant) {\n groupQuery = groupQuery.select('b.tenant_id as tenant_id' as any).groupBy('b.tenant_id' as any)\n }\n if (groupByOrg) {\n groupQuery = groupQuery.select('b.organization_id as organization_id' as any).groupBy('b.organization_id' as any)\n }\n const rows = await groupQuery.execute() as Array<Record<string, unknown>>\n for (const row of rows) {\n const bucketTenant = groupByTenant\n ? ((row as any)?.tenant_id ?? null)\n : (tenantId === undefined ? null : tenantId ?? null)\n const bucketOrg = groupByOrg\n ? ((row as any)?.organization_id ?? null)\n : (organizationId === undefined ? null : organizationId ?? null)\n registerBaseCount(bucketTenant, bucketOrg, toNumber((row as any)?.count))\n }\n } else {\n const row = await applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n ).executeTakeFirst() as { count: unknown } | undefined\n const bucketTenant = tenantId === undefined ? null : tenantId ?? null\n const bucketOrg = organizationId === undefined ? null : organizationId ?? null\n registerBaseCount(bucketTenant, bucketOrg, toNumber(row?.count))\n }\n\n const total = Array.from(baseCounts.values()).reduce((acc, value) => acc + (Number.isFinite(value.count) ? value.count : 0), 0)\n await prepareJob(db, jobScope, 'reindexing', { totalCount: total })\n const jobRow = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['started_at' as any])\n .where('entity_type' as any, '=', entityType)\n .where('organization_id' as any, 'is', null as any)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .orderBy('started_at' as any, 'desc')\n .executeTakeFirst() as { started_at: Date | string } | undefined\n const jobStartedAt = jobRow?.started_at ? new Date(jobRow.started_at) : new Date()\n const deriveOrg = deriveOrgFromId.has(entityType)\n ? (row: AnyRow) => String(row.id)\n : undefined\n\n const scopeOverrides: { tenantId?: string; orgId?: string } = {}\n if (tenantId !== undefined && tenantId !== null) {\n scopeOverrides.tenantId = String(tenantId)\n }\n if (organizationId !== undefined && organizationId !== null) {\n scopeOverrides.orgId = String(organizationId)\n }\n\n const scopeEntries = Array.from(baseCounts.values()).map((entry) => ({\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n }))\n const tenantScopes = Array.from(\n new Set(scopeEntries.map((entry) => entry.tenantId ?? null)),\n )\n\n let processed = 0\n let lastId: string | null = null\n let jobFailed = false\n const writeTotals = createEmptyUpsertIndexBatchResult()\n\n options?.onProgress?.({ processed, total, chunkSize: 0 })\n\n if (resetCoverage) {\n if (force) {\n try {\n let purgeQuery = db\n .deleteFrom('entity_indexes' as any)\n .where('entity_type' as any, '=', entityType)\n if (tenantId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n }\n if (organizationId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`organization_id is not distinct from ${organizationId ?? null}`)\n }\n await purgeQuery.execute()\n } catch (error) {\n logger.warn('Failed to purge index rows before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n }\n\n if (emitVectorize && eventBus) {\n if (tenantId !== undefined) {\n const payload: Record<string, unknown> = {\n entityType,\n tenantId: tenantId ?? null,\n }\n if (organizationId !== undefined) payload.organizationId = organizationId ?? null\n try {\n await eventBus.emitEvent('query_index.vectorize_purge', payload)\n } catch (err) {\n logger.warn('Failed to queue vector purge before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: err instanceof Error ? err.message : err,\n })\n }\n } else {\n logger.warn('Skipping vector purge for force reindex without tenant scope', {\n entityType,\n })\n }\n }\n }\n\n const nowTs = Date.now()\n for (const scope of baseCounts.values()) {\n const key = `${entityType}|${scopeKey(scope.tenantId, scope.organizationId)}`\n const last = lastCoverageReset.get(key) ?? 0\n if (force || nowTs - last >= COVERAGE_REFRESH_THROTTLE_MS) {\n await writeCoverageCounts(em, {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n }, {\n baseCount: scope.count,\n indexedCount: 0,\n vectorCount: emitVectorize ? 0 : undefined,\n })\n lastCoverageReset.set(key, nowTs)\n }\n }\n }\n\n try {\n while (true) {\n let query = applyBaseWhere(\n db\n .selectFrom(`${table} as b` as any)\n .selectAll('b' as any)\n .orderBy('b.id' as any, 'asc')\n .limit(batchSize),\n )\n if (lastId !== null) {\n query = query.where('b.id' as any, '>', lastId)\n }\n const rows = await query.execute() as AnyRow[]\n if (!rows.length) break\n\n const encryption = resolveTenantEncryptionService(em as any)\n const dekKeyCache = new Map<string | null, string | null>()\n const encryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n return await encryptIndexDocForStorage(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n )\n }\n const decryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n const result = await decryptIndexDocForSearch(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n dekKeyCache,\n )\n if (isSearchDebugEnabled()) {\n logger.debug('Reindex decrypt', buildReindexDecryptDebugPayload(targetEntity, result as Record<string, unknown>, scope))\n }\n return result\n }\n\n const batchResult = await upsertIndexBatch(db, entityType, rows, scopeOverrides, { deriveOrganizationId: deriveOrg, encryptDoc, decryptDoc })\n mergeUpsertIndexBatchResults(writeTotals, batchResult)\n\n // A whole batch failing is infrastructural (pool exhausted, disk full, KMS down),\n // not a poison record. Abort now instead of grinding through the rest of the table.\n if (batchResult.written === 0 && batchResult.attempted > 0) {\n throw new QueryIndexBatchWriteError(entityType, writeTotals)\n }\n\n const failedInBatch = new Set(batchResult.failedRecordIds)\n const writtenRows = failedInBatch.size ? rows.filter((row) => !failedInBatch.has(String(row.id))) : rows\n\n const coverageDeltas = new Map<string, { tenantId: string | null; organizationId: string | null; delta: number }>()\n for (const row of writtenRows) {\n const scopeTenant = tenantId !== undefined\n ? tenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n const scopeOrg = organizationId !== undefined\n ? organizationId ?? null\n : (hasOrgCol ? ((row as AnyRow).organization_id ?? null) : (deriveOrg ? deriveOrg(row) ?? null : null))\n const key = scopeKey(scopeTenant ?? null, scopeOrg ?? null)\n const existingDelta = coverageDeltas.get(key)\n if (existingDelta) existingDelta.delta += 1\n else coverageDeltas.set(key, {\n tenantId: scopeTenant ?? null,\n organizationId: scopeOrg ?? null,\n delta: 1,\n })\n }\n if (coverageDeltas.size > 0) {\n await applyCoverageAdjustments(\n em,\n Array.from(coverageDeltas.values()).map((entry) => ({\n entityType,\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n withDeleted: false,\n deltaBase: 0,\n deltaIndex: entry.delta,\n })),\n )\n }\n\n if (emitVectorize && eventBus) {\n await Promise.all(\n writtenRows.map((row) => {\n const scopeOrg = organizationId !== undefined\n ? organizationId ?? null\n : hasOrgCol\n ? ((row as AnyRow).organization_id ?? null)\n : (deriveOrg ? deriveOrg(row) ?? null : null)\n const scopeTenant = tenantId !== undefined\n ? tenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n return eventBus\n .emitEvent('query_index.vectorize_one', {\n entityType,\n recordId: String(row.id),\n organizationId: scopeOrg,\n tenantId: scopeTenant,\n })\n .catch(() => undefined)\n }),\n )\n }\n\n processed += batchResult.written\n lastId = String(rows[rows.length - 1]!.id)\n options?.onProgress?.({ processed, total, chunkSize: batchResult.written })\n await updateJobProgress(db, jobScope, batchResult.written)\n }\n\n // Records this run failed to write still look untouched to the purge predicate, so\n // without the exclusion the purge would delete the index rows it just failed to\n // rebuild \u2014 turning a stale entry into a missing one.\n const purgeExclusions = writeTotals.failedRecordIds\n if (purgeExclusions.length > MAX_PURGE_EXCLUSIONS) {\n logger.warn('Skipping orphan purge after widespread write failures', {\n entityType,\n failedRecords: purgeExclusions.length,\n })\n } else {\n await purgeOrphans(db, {\n entityType,\n tenantId,\n organizationId,\n partitionIndex: usingPartitions ? partitionIndex : null,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n startedAt: jobStartedAt,\n excludeRecordIds: purgeExclusions,\n })\n }\n\n if (force && vectorService && (!usingPartitions || partitionIndex === null)) {\n try {\n await vectorService.removeOrphans({\n entityId: entityType,\n tenantId,\n organizationId,\n olderThan: jobStartedAt,\n })\n } catch (error) {\n logger.warn('Failed to prune vector orphans after reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n for (const scope of scopeEntries) {\n await refreshCoverageSnapshot(\n em,\n {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n },\n )\n }\n\n // Deliberately after the coverage refresh: the authoritative recount is what keeps\n // indexed_count truthful, and it is most worth having when a run has just failed.\n // Throwing here fails the queue job so the loss is visible instead of silent.\n if (writeTotals.searchTokenFailures > 0) {\n logger.warn('Search token writes failed during reindex', {\n entityType,\n batches: writeTotals.searchTokenFailures,\n })\n }\n assertIndexBatchWritesLanded(entityType, writeTotals)\n } catch (error) {\n jobFailed = true\n throw error\n } finally {\n // Still finalized on failure: the scope stays wedged behind the active-job guard\n // while finished_at is null. The status carries the outcome instead.\n await finalizeJob(db, jobScope, jobFailed ? { status: 'failed' } : {})\n }\n\n return {\n processed,\n total,\n scopes: scopeEntries,\n tenantScopes,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -12,6 +12,9 @@ async function purgeOrphans(db, options) {
12
12
  q = q.where(sql`mod(abs(hashtext(entity_id::text)), ${partitionCount}) = ${partitionIndex}`);
13
13
  }
14
14
  q = q.where("updated_at", "<", startedAt);
15
+ if (options.excludeRecordIds?.length) {
16
+ q = q.where("entity_id", "not in", options.excludeRecordIds);
17
+ }
15
18
  await q.execute();
16
19
  }
17
20
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/lib/stale.ts"],
4
- "sourcesContent": ["import { type Kysely, sql } from 'kysely'\n\ntype PurgeOrphansOptions = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n partitionIndex: number | null\n partitionCount: number | null\n startedAt: Date\n}\n\nexport async function purgeOrphans(\n db: Kysely<any>,\n options: PurgeOrphansOptions,\n): Promise<void> {\n const { entityType, tenantId, partitionIndex, partitionCount, startedAt } = options\n let q = db.deleteFrom('entity_indexes' as any).where('entity_type' as any, '=', entityType)\n if (tenantId !== undefined) {\n q = q.where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n }\n if (options.organizationId !== undefined) {\n q = q.where(sql<boolean>`organization_id is not distinct from ${options.organizationId ?? null}`)\n }\n if (partitionIndex != null && partitionCount != null) {\n q = q.where(sql<boolean>`mod(abs(hashtext(entity_id::text)), ${partitionCount}) = ${partitionIndex}`)\n }\n q = q.where('updated_at' as any, '<', startedAt as any)\n await q.execute()\n}\n"],
5
- "mappings": "AAAA,SAAsB,WAAW;AAWjC,eAAsB,aACpB,IACA,SACe;AACf,QAAM,EAAE,YAAY,UAAU,gBAAgB,gBAAgB,UAAU,IAAI;AAC5E,MAAI,IAAI,GAAG,WAAW,gBAAuB,EAAE,MAAM,eAAsB,KAAK,UAAU;AAC1F,MAAI,aAAa,QAAW;AAC1B,QAAI,EAAE,MAAM,qCAA8C,YAAY,IAAI,EAAE;AAAA,EAC9E;AACA,MAAI,QAAQ,mBAAmB,QAAW;AACxC,QAAI,EAAE,MAAM,2CAAoD,QAAQ,kBAAkB,IAAI,EAAE;AAAA,EAClG;AACA,MAAI,kBAAkB,QAAQ,kBAAkB,MAAM;AACpD,QAAI,EAAE,MAAM,0CAAmD,cAAc,OAAO,cAAc,EAAE;AAAA,EACtG;AACA,MAAI,EAAE,MAAM,cAAqB,KAAK,SAAgB;AACtD,QAAM,EAAE,QAAQ;AAClB;",
4
+ "sourcesContent": ["import { type Kysely, sql } from 'kysely'\n\ntype PurgeOrphansOptions = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n partitionIndex: number | null\n partitionCount: number | null\n startedAt: Date\n /**\n * Records whose index rows must be preserved even though this run did not refresh\n * them. A row that failed to be written still looks untouched to the `updated_at`\n * predicate below, so without this the purge would delete the very entries the run\n * failed to rebuild.\n */\n excludeRecordIds?: string[]\n}\n\nexport async function purgeOrphans(\n db: Kysely<any>,\n options: PurgeOrphansOptions,\n): Promise<void> {\n const { entityType, tenantId, partitionIndex, partitionCount, startedAt } = options\n let q = db.deleteFrom('entity_indexes' as any).where('entity_type' as any, '=', entityType)\n if (tenantId !== undefined) {\n q = q.where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n }\n if (options.organizationId !== undefined) {\n q = q.where(sql<boolean>`organization_id is not distinct from ${options.organizationId ?? null}`)\n }\n if (partitionIndex != null && partitionCount != null) {\n q = q.where(sql<boolean>`mod(abs(hashtext(entity_id::text)), ${partitionCount}) = ${partitionIndex}`)\n }\n q = q.where('updated_at' as any, '<', startedAt as any)\n if (options.excludeRecordIds?.length) {\n q = q.where('entity_id' as any, 'not in', options.excludeRecordIds)\n }\n await q.execute()\n}\n"],
5
+ "mappings": "AAAA,SAAsB,WAAW;AAkBjC,eAAsB,aACpB,IACA,SACe;AACf,QAAM,EAAE,YAAY,UAAU,gBAAgB,gBAAgB,UAAU,IAAI;AAC5E,MAAI,IAAI,GAAG,WAAW,gBAAuB,EAAE,MAAM,eAAsB,KAAK,UAAU;AAC1F,MAAI,aAAa,QAAW;AAC1B,QAAI,EAAE,MAAM,qCAA8C,YAAY,IAAI,EAAE;AAAA,EAC9E;AACA,MAAI,QAAQ,mBAAmB,QAAW;AACxC,QAAI,EAAE,MAAM,2CAAoD,QAAQ,kBAAkB,IAAI,EAAE;AAAA,EAClG;AACA,MAAI,kBAAkB,QAAQ,kBAAkB,MAAM;AACpD,QAAI,EAAE,MAAM,0CAAmD,cAAc,OAAO,cAAc,EAAE;AAAA,EACtG;AACA,MAAI,EAAE,MAAM,cAAqB,KAAK,SAAgB;AACtD,MAAI,QAAQ,kBAAkB,QAAQ;AACpC,QAAI,EAAE,MAAM,aAAoB,UAAU,QAAQ,gBAAgB;AAAA,EACpE;AACA,QAAM,EAAE,QAAQ;AAClB;",
6
6
  "names": []
7
7
  }
@@ -1,20 +1,49 @@
1
- import { resolveEntityTableName } from "@open-mercato/shared/lib/query/engine";
1
+ import { getEntityIds } from "@open-mercato/shared/lib/encryption/entityIds";
2
+ import { resolveRegisteredEntityTableName } from "@open-mercato/shared/lib/query/engine";
2
3
  class QueryIndexScopeError extends Error {
3
4
  constructor(message) {
4
5
  super(message);
5
6
  this.name = "QueryIndexScopeError";
6
7
  }
7
8
  }
8
- async function loadQueryIndexRowScope(em, entityType, recordId) {
9
+ function resolveQueryIndexSourceMetadata(em, entityType) {
10
+ const registeredEntityIds = Object.values(getEntityIds(false)).flatMap((moduleEntities) => Object.values(moduleEntities ?? {}));
11
+ if (!registeredEntityIds.includes(entityType)) {
12
+ throw new QueryIndexScopeError(`Query index entity type is not registered: ${entityType}`);
13
+ }
14
+ const table = resolveRegisteredEntityTableName(em, entityType);
15
+ if (!table) {
16
+ throw new QueryIndexScopeError(`Query index entity type has no registered ORM table: ${entityType}`);
17
+ }
18
+ const registry = em.getMetadata?.();
19
+ const allMetadataRaw = typeof registry?.getAll === "function" ? registry.getAll() : null;
20
+ const allMetadata = Array.isArray(allMetadataRaw) ? allMetadataRaw : allMetadataRaw instanceof Map ? Array.from(allMetadataRaw.values()) : Object.values(allMetadataRaw ?? {});
21
+ const metadata = allMetadata.find((candidate) => String(candidate.tableName ?? "") === table);
22
+ if (!metadata) {
23
+ throw new QueryIndexScopeError(`Query index entity metadata was not found for table: ${table}`);
24
+ }
25
+ return {
26
+ table,
27
+ organizationColumn: resolveScopeColumn(metadata, "organizationId", table),
28
+ tenantColumn: resolveScopeColumn(metadata, "tenantId", table)
29
+ };
30
+ }
31
+ async function loadQueryIndexRowScope(em, source, recordId) {
32
+ if (!source.organizationColumn && !source.tenantColumn) {
33
+ return { kind: "global" };
34
+ }
9
35
  const db = em.getKysely();
10
- const table = resolveEntityTableName(em, entityType);
11
- const row = await db.selectFrom(table).select(["organization_id", "tenant_id"]).where("id", "=", recordId).executeTakeFirst();
36
+ const columns = [source.organizationColumn, source.tenantColumn].filter((column) => column !== null);
37
+ const row = await db.selectFrom(source.table).select(columns).where("id", "=", recordId).executeTakeFirst();
12
38
  if (!row) {
13
- return null;
39
+ return { kind: "missing" };
14
40
  }
15
41
  return {
16
- organizationId: row.organization_id ?? null,
17
- tenantId: row.tenant_id ?? null
42
+ kind: "row",
43
+ scope: {
44
+ organizationId: source.organizationColumn ? row[source.organizationColumn] ?? null : null,
45
+ tenantId: source.tenantColumn ? row[source.tenantColumn] ?? null : null
46
+ }
18
47
  };
19
48
  }
20
49
  function resolveQueryIndexRecordScope(input) {
@@ -23,9 +52,20 @@ function resolveQueryIndexRecordScope(input) {
23
52
  payloadOrganizationId,
24
53
  hasPayloadTenantId,
25
54
  hasPayloadOrganizationId,
26
- rowScope
55
+ sourceScope
27
56
  } = input;
28
- if (!rowScope) {
57
+ if (sourceScope.kind === "global") {
58
+ if (!hasPayloadTenantId || !hasPayloadOrganizationId || payloadTenantId !== null || payloadOrganizationId !== null) {
59
+ throw new QueryIndexScopeError(
60
+ "Query index event for a global entity must explicitly provide tenantId and organizationId as null"
61
+ );
62
+ }
63
+ return {
64
+ tenantId: null,
65
+ organizationId: null
66
+ };
67
+ }
68
+ if (sourceScope.kind === "missing") {
29
69
  if (!hasPayloadTenantId || !hasPayloadOrganizationId) {
30
70
  throw new QueryIndexScopeError(
31
71
  "Query index event is missing tenantId/organizationId and source row scope could not be resolved"
@@ -36,6 +76,7 @@ function resolveQueryIndexRecordScope(input) {
36
76
  organizationId: payloadOrganizationId ?? null
37
77
  };
38
78
  }
79
+ const rowScope = sourceScope.scope;
39
80
  if (hasPayloadTenantId && !isSameScopeValue(payloadTenantId, rowScope.tenantId)) {
40
81
  throw new QueryIndexScopeError(
41
82
  `Query index event tenantId does not match source row scope (payload=${String(payloadTenantId ?? null)}, row=${String(rowScope.tenantId)})`
@@ -65,10 +106,22 @@ function resolveQueryIndexReindexScope(input) {
65
106
  function isSameScopeValue(left, right) {
66
107
  return (left ?? null) === right;
67
108
  }
109
+ function resolveScopeColumn(metadata, property, table) {
110
+ const scopeProperty = metadata.properties?.[property];
111
+ if (!scopeProperty) return null;
112
+ const fieldNames = scopeProperty.fieldNames;
113
+ if (!Array.isArray(fieldNames) || fieldNames.length !== 1 || typeof fieldNames[0] !== "string" || !fieldNames[0]) {
114
+ throw new QueryIndexScopeError(
115
+ `Query index ${property} metadata must map exactly one physical column for table: ${table}`
116
+ );
117
+ }
118
+ return fieldNames[0];
119
+ }
68
120
  export {
69
121
  QueryIndexScopeError,
70
122
  loadQueryIndexRowScope,
71
123
  resolveQueryIndexRecordScope,
72
- resolveQueryIndexReindexScope
124
+ resolveQueryIndexReindexScope,
125
+ resolveQueryIndexSourceMetadata
73
126
  };
74
127
  //# sourceMappingURL=subscriber-scope.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/lib/subscriber-scope.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\n\nexport type QueryIndexScope = {\n tenantId: string | null\n organizationId: string | null\n}\n\nexport class QueryIndexScopeError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'QueryIndexScopeError'\n }\n}\n\ntype ResolveRecordScopeInput = {\n payloadTenantId: string | null | undefined\n payloadOrganizationId: string | null | undefined\n hasPayloadTenantId: boolean\n hasPayloadOrganizationId: boolean\n rowScope: QueryIndexScope | null\n}\n\ntype ResolveReindexScopeInput = {\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n allowAllTenants?: boolean\n}\n\nexport async function loadQueryIndexRowScope(\n em: EntityManager,\n entityType: string,\n recordId: string\n): Promise<QueryIndexScope | null> {\n const db = em.getKysely<any>()\n const table = resolveEntityTableName(em, entityType)\n const row = await db\n .selectFrom(table as any)\n .select(['organization_id' as any, 'tenant_id' as any])\n .where('id' as any, '=', recordId)\n .executeTakeFirst() as { organization_id: string | null; tenant_id: string | null } | undefined\n\n if (!row) {\n return null\n }\n\n return {\n organizationId: row.organization_id ?? null,\n tenantId: row.tenant_id ?? null,\n }\n}\n\nexport function resolveQueryIndexRecordScope(input: ResolveRecordScopeInput): QueryIndexScope {\n const {\n payloadTenantId,\n payloadOrganizationId,\n hasPayloadTenantId,\n hasPayloadOrganizationId,\n rowScope,\n } = input\n\n if (!rowScope) {\n if (!hasPayloadTenantId || !hasPayloadOrganizationId) {\n throw new QueryIndexScopeError(\n 'Query index event is missing tenantId/organizationId and source row scope could not be resolved'\n )\n }\n\n return {\n tenantId: payloadTenantId ?? null,\n organizationId: payloadOrganizationId ?? null,\n }\n }\n\n if (hasPayloadTenantId && !isSameScopeValue(payloadTenantId, rowScope.tenantId)) {\n throw new QueryIndexScopeError(\n `Query index event tenantId does not match source row scope (payload=${String(payloadTenantId ?? null)}, row=${String(rowScope.tenantId)})`\n )\n }\n\n if (hasPayloadOrganizationId && !isSameScopeValue(payloadOrganizationId, rowScope.organizationId)) {\n throw new QueryIndexScopeError(\n `Query index event organizationId does not match source row scope (payload=${String(payloadOrganizationId ?? null)}, row=${String(rowScope.organizationId)})`\n )\n }\n\n return {\n tenantId: hasPayloadTenantId ? (payloadTenantId ?? null) : rowScope.tenantId,\n organizationId: hasPayloadOrganizationId ? (payloadOrganizationId ?? null) : rowScope.organizationId,\n }\n}\n\nexport function resolveQueryIndexReindexScope(input: ResolveReindexScopeInput): {\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n} {\n if (input.tenantId === undefined && input.allowAllTenants !== true) {\n throw new QueryIndexScopeError(\n 'Query index reindex requires tenantId to be set explicitly; all-tenant reindex must opt in with allowAllTenants=true'\n )\n }\n\n return {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }\n}\n\nfunction isSameScopeValue(left: string | null | undefined, right: string | null): boolean {\n return (left ?? null) === right\n}\n"],
5
- "mappings": "AACA,SAAS,8BAA8B;AAOhC,MAAM,6BAA6B,MAAM;AAAA,EAC9C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAgBA,eAAsB,uBACpB,IACA,YACA,UACiC;AACjC,QAAM,KAAK,GAAG,UAAe;AAC7B,QAAM,QAAQ,uBAAuB,IAAI,UAAU;AACnD,QAAM,MAAM,MAAM,GACf,WAAW,KAAY,EACvB,OAAO,CAAC,mBAA0B,WAAkB,CAAC,EACrD,MAAM,MAAa,KAAK,QAAQ,EAChC,iBAAiB;AAEpB,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,gBAAgB,IAAI,mBAAmB;AAAA,IACvC,UAAU,IAAI,aAAa;AAAA,EAC7B;AACF;AAEO,SAAS,6BAA6B,OAAiD;AAC5F,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,MAAI,CAAC,UAAU;AACb,QAAI,CAAC,sBAAsB,CAAC,0BAA0B;AACpD,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,UAAU,mBAAmB;AAAA,MAC7B,gBAAgB,yBAAyB;AAAA,IAC3C;AAAA,EACF;AAEA,MAAI,sBAAsB,CAAC,iBAAiB,iBAAiB,SAAS,QAAQ,GAAG;AAC/E,UAAM,IAAI;AAAA,MACR,uEAAuE,OAAO,mBAAmB,IAAI,CAAC,SAAS,OAAO,SAAS,QAAQ,CAAC;AAAA,IAC1I;AAAA,EACF;AAEA,MAAI,4BAA4B,CAAC,iBAAiB,uBAAuB,SAAS,cAAc,GAAG;AACjG,UAAM,IAAI;AAAA,MACR,6EAA6E,OAAO,yBAAyB,IAAI,CAAC,SAAS,OAAO,SAAS,cAAc,CAAC;AAAA,IAC5J;AAAA,EACF;AAEA,SAAO;AAAA,IACL,UAAU,qBAAsB,mBAAmB,OAAQ,SAAS;AAAA,IACpE,gBAAgB,2BAA4B,yBAAyB,OAAQ,SAAS;AAAA,EACxF;AACF;AAEO,SAAS,8BAA8B,OAG5C;AACA,MAAI,MAAM,aAAa,UAAa,MAAM,oBAAoB,MAAM;AAClE,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB;AACF;AAEA,SAAS,iBAAiB,MAAiC,OAA+B;AACxF,UAAQ,QAAQ,UAAU;AAC5B;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { resolveRegisteredEntityTableName } from '@open-mercato/shared/lib/query/engine'\n\nexport type QueryIndexScope = {\n tenantId: string | null\n organizationId: string | null\n}\n\nexport type QueryIndexSourceMetadata = {\n table: string\n organizationColumn: string | null\n tenantColumn: string | null\n}\n\nexport type QueryIndexSourceScope =\n | { kind: 'global' }\n | { kind: 'missing' }\n | { kind: 'row'; scope: QueryIndexScope }\n\ntype QueryIndexEntityMetadata = {\n tableName?: unknown\n properties?: Record<string, { fieldNames?: unknown }>\n}\n\nexport class QueryIndexScopeError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'QueryIndexScopeError'\n }\n}\n\ntype ResolveRecordScopeInput = {\n payloadTenantId: string | null | undefined\n payloadOrganizationId: string | null | undefined\n hasPayloadTenantId: boolean\n hasPayloadOrganizationId: boolean\n sourceScope: QueryIndexSourceScope\n}\n\ntype ResolveReindexScopeInput = {\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n allowAllTenants?: boolean\n}\n\nexport function resolveQueryIndexSourceMetadata(\n em: EntityManager,\n entityType: string,\n): QueryIndexSourceMetadata {\n const registeredEntityIds = Object.values(getEntityIds(false)).flatMap((moduleEntities) => Object.values(moduleEntities ?? {}))\n if (!registeredEntityIds.includes(entityType)) {\n throw new QueryIndexScopeError(`Query index entity type is not registered: ${entityType}`)\n }\n\n const table = resolveRegisteredEntityTableName(em, entityType as never)\n if (!table) {\n throw new QueryIndexScopeError(`Query index entity type has no registered ORM table: ${entityType}`)\n }\n\n const registry = em.getMetadata?.()\n const allMetadataRaw = typeof registry?.getAll === 'function' ? registry.getAll() : null\n const allMetadata: QueryIndexEntityMetadata[] = Array.isArray(allMetadataRaw)\n ? allMetadataRaw as QueryIndexEntityMetadata[]\n : allMetadataRaw instanceof Map\n ? Array.from(allMetadataRaw.values()) as QueryIndexEntityMetadata[]\n : Object.values(allMetadataRaw ?? {}) as QueryIndexEntityMetadata[]\n const metadata = allMetadata.find((candidate) => String(candidate.tableName ?? '') === table)\n if (!metadata) {\n throw new QueryIndexScopeError(`Query index entity metadata was not found for table: ${table}`)\n }\n\n return {\n table,\n organizationColumn: resolveScopeColumn(metadata, 'organizationId', table),\n tenantColumn: resolveScopeColumn(metadata, 'tenantId', table),\n }\n}\n\nexport async function loadQueryIndexRowScope(\n em: EntityManager,\n source: QueryIndexSourceMetadata,\n recordId: string,\n): Promise<QueryIndexSourceScope> {\n if (!source.organizationColumn && !source.tenantColumn) {\n return { kind: 'global' }\n }\n\n const db = em.getKysely<any>()\n const columns = [source.organizationColumn, source.tenantColumn].filter((column): column is string => column !== null)\n const row = await db\n .selectFrom(source.table as any)\n .select(columns as any)\n .where('id' as any, '=', recordId)\n .executeTakeFirst() as Record<string, string | null | undefined> | undefined\n\n if (!row) {\n return { kind: 'missing' }\n }\n\n return {\n kind: 'row',\n scope: {\n organizationId: source.organizationColumn ? row[source.organizationColumn] ?? null : null,\n tenantId: source.tenantColumn ? row[source.tenantColumn] ?? null : null,\n },\n }\n}\n\nexport function resolveQueryIndexRecordScope(input: ResolveRecordScopeInput): QueryIndexScope {\n const {\n payloadTenantId,\n payloadOrganizationId,\n hasPayloadTenantId,\n hasPayloadOrganizationId,\n sourceScope,\n } = input\n\n if (sourceScope.kind === 'global') {\n if (!hasPayloadTenantId || !hasPayloadOrganizationId || payloadTenantId !== null || payloadOrganizationId !== null) {\n throw new QueryIndexScopeError(\n 'Query index event for a global entity must explicitly provide tenantId and organizationId as null'\n )\n }\n\n return {\n tenantId: null,\n organizationId: null,\n }\n }\n\n if (sourceScope.kind === 'missing') {\n if (!hasPayloadTenantId || !hasPayloadOrganizationId) {\n throw new QueryIndexScopeError(\n 'Query index event is missing tenantId/organizationId and source row scope could not be resolved'\n )\n }\n\n return {\n tenantId: payloadTenantId ?? null,\n organizationId: payloadOrganizationId ?? null,\n }\n }\n\n const rowScope = sourceScope.scope\n\n if (hasPayloadTenantId && !isSameScopeValue(payloadTenantId, rowScope.tenantId)) {\n throw new QueryIndexScopeError(\n `Query index event tenantId does not match source row scope (payload=${String(payloadTenantId ?? null)}, row=${String(rowScope.tenantId)})`\n )\n }\n\n if (hasPayloadOrganizationId && !isSameScopeValue(payloadOrganizationId, rowScope.organizationId)) {\n throw new QueryIndexScopeError(\n `Query index event organizationId does not match source row scope (payload=${String(payloadOrganizationId ?? null)}, row=${String(rowScope.organizationId)})`\n )\n }\n\n return {\n tenantId: hasPayloadTenantId ? (payloadTenantId ?? null) : rowScope.tenantId,\n organizationId: hasPayloadOrganizationId ? (payloadOrganizationId ?? null) : rowScope.organizationId,\n }\n}\n\nexport function resolveQueryIndexReindexScope(input: ResolveReindexScopeInput): {\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n} {\n if (input.tenantId === undefined && input.allowAllTenants !== true) {\n throw new QueryIndexScopeError(\n 'Query index reindex requires tenantId to be set explicitly; all-tenant reindex must opt in with allowAllTenants=true'\n )\n }\n\n return {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }\n}\n\nfunction isSameScopeValue(left: string | null | undefined, right: string | null): boolean {\n return (left ?? null) === right\n}\n\nfunction resolveScopeColumn(metadata: QueryIndexEntityMetadata, property: string, table: string): string | null {\n const scopeProperty = metadata.properties?.[property]\n if (!scopeProperty) return null\n const fieldNames = scopeProperty.fieldNames\n if (!Array.isArray(fieldNames) || fieldNames.length !== 1 || typeof fieldNames[0] !== 'string' || !fieldNames[0]) {\n throw new QueryIndexScopeError(\n `Query index ${property} metadata must map exactly one physical column for table: ${table}`\n )\n }\n return fieldNames[0]\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,12 @@
1
1
  import { recordIndexerError } from "@open-mercato/shared/lib/indexers/error-log";
2
- import { resolveEntityTableName } from "@open-mercato/shared/lib/query/engine";
3
2
  import { sql } from "kysely";
4
3
  import { markDeleted } from "../lib/indexer.js";
5
4
  import { applyCoverageAdjustments, createCoverageAdjustments } from "../lib/coverage.js";
6
- import { loadQueryIndexRowScope, resolveQueryIndexRecordScope } from "../lib/subscriber-scope.js";
5
+ import {
6
+ loadQueryIndexRowScope,
7
+ resolveQueryIndexRecordScope,
8
+ resolveQueryIndexSourceMetadata
9
+ } from "../lib/subscriber-scope.js";
7
10
  const metadata = { event: "query_index.delete_one", persistent: false };
8
11
  const DELETE_COVERAGE_THROTTLE_MS = 5 * 60 * 1e3;
9
12
  const lastDeleteCoverageRefreshAt = /* @__PURE__ */ new Map();
@@ -28,13 +31,14 @@ async function handle(payload, ctx) {
28
31
  try {
29
32
  const hasPayloadOrganizationId = Object.prototype.hasOwnProperty.call(payload ?? {}, "organizationId");
30
33
  const hasPayloadTenantId = Object.prototype.hasOwnProperty.call(payload ?? {}, "tenantId");
31
- const rowScope = await loadQueryIndexRowScope(em, entityType, recordId).catch(() => null);
34
+ const source = resolveQueryIndexSourceMetadata(em, entityType);
35
+ const sourceScope = await loadQueryIndexRowScope(em, source, recordId);
32
36
  const resolvedScope = resolveQueryIndexRecordScope({
33
37
  payloadOrganizationId: payload?.organizationId,
34
38
  payloadTenantId: payload?.tenantId,
35
39
  hasPayloadOrganizationId,
36
40
  hasPayloadTenantId,
37
- rowScope
41
+ sourceScope
38
42
  });
39
43
  organizationId = resolvedScope.organizationId;
40
44
  tenantId = resolvedScope.tenantId;
@@ -43,8 +47,18 @@ async function handle(payload, ctx) {
43
47
  let baseCheckSucceeded = false;
44
48
  try {
45
49
  const db = em.getKysely();
46
- const table = resolveEntityTableName(em, entityType);
47
- const row = await db.selectFrom(table).select(["deleted_at"]).where("id", "=", recordId).where("organization_id", organizationId === null ? "is" : "=", organizationId).where(sql`tenant_id is not distinct from ${tenantId}`).executeTakeFirst();
50
+ let baseQuery = db.selectFrom(source.table).select(["deleted_at"]).where("id", "=", recordId);
51
+ if (source.organizationColumn) {
52
+ baseQuery = baseQuery.where(
53
+ source.organizationColumn,
54
+ organizationId === null ? "is" : "=",
55
+ organizationId
56
+ );
57
+ }
58
+ if (source.tenantColumn) {
59
+ baseQuery = baseQuery.where(sql`${sql.ref(source.tenantColumn)} is not distinct from ${tenantId}`);
60
+ }
61
+ const row = await baseQuery.executeTakeFirst();
48
62
  const baseMissing = !row;
49
63
  const baseDeleted = baseMissing || row && row.deleted_at != null;
50
64
  baseCheckSucceeded = true;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/subscribers/delete_one.ts"],
4
- "sourcesContent": ["import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { sql } from 'kysely'\nimport { markDeleted } from '../lib/indexer'\nimport { applyCoverageAdjustments, createCoverageAdjustments } from '../lib/coverage'\nimport { loadQueryIndexRowScope, resolveQueryIndexRecordScope } from '../lib/subscriber-scope'\n\nexport const metadata = { event: 'query_index.delete_one', persistent: false }\n\n// Mirrors `shouldTriggerCoverageRefresh()` in `@open-mercato/shared/lib/data/engine.ts`\n// as a small local throttle so per-record deletes stop firing an unconditional, immediate\n// full recount. Note: this Map and the shared engine's own throttle Map are independent,\n// so a rare race right after a 5-minute window resets on both could double-fire once \u2014\n// harmless (the recompute is idempotent) and far cheaper than today's per-delete cost.\nconst DELETE_COVERAGE_THROTTLE_MS = 5 * 60 * 1000\nconst lastDeleteCoverageRefreshAt = new Map<string, number>()\n\nfunction shouldAllowDeleteCoverageRefresh(entityType: string, tenantId: string | null): boolean {\n if (!entityType) return false\n const key = `${entityType}|${tenantId ?? '__null__'}`\n const now = Date.now()\n const last = lastDeleteCoverageRefreshAt.get(key) ?? 0\n if (now - last < DELETE_COVERAGE_THROTTLE_MS) return false\n lastDeleteCoverageRefreshAt.set(key, now)\n return true\n}\n\nexport default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {\n // Forked EntityManager \u2014 this awaited subscriber runs synchronously on the request\n // `em`; isolating it prevents our queries/writes from resetting the originating CRUD\n // write's UnitOfWork and dropping its pending changes. See upsert_one.ts for detail.\n const baseEm = ctx.resolve<any>('em')\n const em = typeof baseEm?.fork === 'function' ? baseEm.fork() : baseEm\n const entityType = String(payload?.entityType || '')\n const recordId = String(payload?.recordId || '')\n if (!entityType || !recordId) return\n let organizationId: string | null = payload?.organizationId ?? null\n let tenantId: string | null = payload?.tenantId ?? null\n const coverageDelayMs = typeof payload?.coverageDelayMs === 'number' ? payload.coverageDelayMs : undefined\n try {\n const hasPayloadOrganizationId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'organizationId')\n const hasPayloadTenantId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'tenantId')\n const rowScope = await loadQueryIndexRowScope(em, entityType, recordId).catch(() => null)\n const resolvedScope = resolveQueryIndexRecordScope({\n payloadOrganizationId: payload?.organizationId,\n payloadTenantId: payload?.tenantId,\n hasPayloadOrganizationId,\n hasPayloadTenantId,\n rowScope,\n })\n organizationId = resolvedScope.organizationId\n tenantId = resolvedScope.tenantId\n\n const { wasActive } = await markDeleted(em, { entityType, recordId, organizationId, tenantId })\n\n let baseDelta = 0\n let baseCheckSucceeded = false\n try {\n const db = (em as any).getKysely()\n const table = resolveEntityTableName(em, entityType)\n const row = await db\n .selectFrom(table as any)\n .select(['deleted_at' as any])\n .where('id' as any, '=', recordId)\n .where('organization_id' as any, organizationId === null ? 'is' : '=', organizationId as any)\n .where(sql`tenant_id is not distinct from ${tenantId}`)\n .executeTakeFirst() as { deleted_at: Date | null } | undefined\n const baseMissing = !row\n const baseDeleted = baseMissing || (row && row.deleted_at != null)\n baseCheckSucceeded = true\n if (baseDeleted) baseDelta = -1\n } catch {}\n if (!baseCheckSucceeded) baseDelta = -1\n\n const baseDeltaOverride =\n typeof payload?.coverageBaseDelta === 'number' ? payload.coverageBaseDelta : undefined\n const indexDeltaOverride =\n typeof payload?.coverageIndexDelta === 'number' ? payload.coverageIndexDelta : undefined\n let effectiveBaseDelta = baseDeltaOverride ?? baseDelta\n let effectiveIndexDelta = indexDeltaOverride ?? (wasActive ? -1 : 0)\n\n if (!Number.isFinite(effectiveBaseDelta)) effectiveBaseDelta = 0\n if (!Number.isFinite(effectiveIndexDelta)) effectiveIndexDelta = 0\n\n if (effectiveBaseDelta !== 0 || effectiveIndexDelta !== 0) {\n const adjustments = createCoverageAdjustments({\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n baseDelta: effectiveBaseDelta,\n indexDelta: effectiveIndexDelta,\n })\n if (adjustments.length) {\n await applyCoverageAdjustments(em, adjustments)\n }\n }\n\n // The projection row + token removal above are synchronous (the data engine\n // awaits this subscriber) so list reads are consistent immediately. The coverage\n // recompute (a COUNT, run inline when delayMs is 0) and the fulltext delete are\n // secondary, so defer them fire-and-forget to keep write/bulk-delete latency bounded.\n const suppressCoverage = payload?.suppressCoverage === true\n const explicitDelayRequested = typeof payload?.coverageDelayMs === 'number'\n const shouldRefreshCoverage =\n !suppressCoverage &&\n (coverageDelayMs === undefined || coverageDelayMs >= 0) &&\n (explicitDelayRequested || shouldAllowDeleteCoverageRefresh(entityType, tenantId))\n const coverageRefreshDelay = coverageDelayMs ?? 0\n void (async () => {\n try {\n const bus = ctx.resolve<any>('eventBus')\n if (shouldRefreshCoverage) {\n await bus.emitEvent('query_index.coverage.refresh', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n delayMs: coverageRefreshDelay,\n })\n }\n await bus.emitEvent('search.delete_record', { entityId: entityType, recordId, organizationId, tenantId })\n } catch (error) {\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.delete_one:coverage_search',\n error,\n entityType,\n recordId,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n ).catch(() => {})\n }\n })()\n } catch (error) {\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.delete_one',\n error,\n entityType,\n recordId,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n )\n throw error\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,WAAW;AACpB,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B,iCAAiC;AACpE,SAAS,wBAAwB,oCAAoC;AAE9D,MAAM,WAAW,EAAE,OAAO,0BAA0B,YAAY,MAAM;AAO7E,MAAM,8BAA8B,IAAI,KAAK;AAC7C,MAAM,8BAA8B,oBAAI,IAAoB;AAE5D,SAAS,iCAAiC,YAAoB,UAAkC;AAC9F,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,MAAM,GAAG,UAAU,IAAI,YAAY,UAAU;AACnD,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,OAAO,4BAA4B,IAAI,GAAG,KAAK;AACrD,MAAI,MAAM,OAAO,4BAA6B,QAAO;AACrD,8BAA4B,IAAI,KAAK,GAAG;AACxC,SAAO;AACT;AAEA,eAAO,OAA8B,SAAc,KAA8C;AAI/F,QAAM,SAAS,IAAI,QAAa,IAAI;AACpC,QAAM,KAAK,OAAO,QAAQ,SAAS,aAAa,OAAO,KAAK,IAAI;AAChE,QAAM,aAAa,OAAO,SAAS,cAAc,EAAE;AACnD,QAAM,WAAW,OAAO,SAAS,YAAY,EAAE;AAC/C,MAAI,CAAC,cAAc,CAAC,SAAU;AAC9B,MAAI,iBAAgC,SAAS,kBAAkB;AAC/D,MAAI,WAA0B,SAAS,YAAY;AACnD,QAAM,kBAAkB,OAAO,SAAS,oBAAoB,WAAW,QAAQ,kBAAkB;AACjG,MAAI;AACF,UAAM,2BAA2B,OAAO,UAAU,eAAe,KAAK,WAAW,CAAC,GAAG,gBAAgB;AACrG,UAAM,qBAAqB,OAAO,UAAU,eAAe,KAAK,WAAW,CAAC,GAAG,UAAU;AACzF,UAAM,WAAW,MAAM,uBAAuB,IAAI,YAAY,QAAQ,EAAE,MAAM,MAAM,IAAI;AACxF,UAAM,gBAAgB,6BAA6B;AAAA,MACjD,uBAAuB,SAAS;AAAA,MAChC,iBAAiB,SAAS;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,qBAAiB,cAAc;AAC/B,eAAW,cAAc;AAEzB,UAAM,EAAE,UAAU,IAAI,MAAM,YAAY,IAAI,EAAE,YAAY,UAAU,gBAAgB,SAAS,CAAC;AAE9F,QAAI,YAAY;AAChB,QAAI,qBAAqB;AACzB,QAAI;AACF,YAAM,KAAM,GAAW,UAAU;AACjC,YAAM,QAAQ,uBAAuB,IAAI,UAAU;AACnD,YAAM,MAAM,MAAM,GACf,WAAW,KAAY,EACvB,OAAO,CAAC,YAAmB,CAAC,EAC5B,MAAM,MAAa,KAAK,QAAQ,EAChC,MAAM,mBAA0B,mBAAmB,OAAO,OAAO,KAAK,cAAqB,EAC3F,MAAM,qCAAqC,QAAQ,EAAE,EACrD,iBAAiB;AACpB,YAAM,cAAc,CAAC;AACrB,YAAM,cAAc,eAAgB,OAAO,IAAI,cAAc;AAC7D,2BAAqB;AACrB,UAAI,YAAa,aAAY;AAAA,IAC/B,QAAQ;AAAA,IAAC;AACT,QAAI,CAAC,mBAAoB,aAAY;AAErC,UAAM,oBACJ,OAAO,SAAS,sBAAsB,WAAW,QAAQ,oBAAoB;AAC/E,UAAM,qBACJ,OAAO,SAAS,uBAAuB,WAAW,QAAQ,qBAAqB;AACjF,QAAI,qBAAqB,qBAAqB;AAC9C,QAAI,sBAAsB,uBAAuB,YAAY,KAAK;AAElE,QAAI,CAAC,OAAO,SAAS,kBAAkB,EAAG,sBAAqB;AAC/D,QAAI,CAAC,OAAO,SAAS,mBAAmB,EAAG,uBAAsB;AAEjE,QAAI,uBAAuB,KAAK,wBAAwB,GAAG;AACzD,YAAM,cAAc,0BAA0B;AAAA,QAC5C;AAAA,QACA,UAAU,YAAY;AAAA,QACtB,gBAAgB,kBAAkB;AAAA,QAClC,WAAW;AAAA,QACX,YAAY;AAAA,MACd,CAAC;AACD,UAAI,YAAY,QAAQ;AACtB,cAAM,yBAAyB,IAAI,WAAW;AAAA,MAChD;AAAA,IACF;AAMA,UAAM,mBAAmB,SAAS,qBAAqB;AACvD,UAAM,yBAAyB,OAAO,SAAS,oBAAoB;AACnE,UAAM,wBACJ,CAAC,qBACA,oBAAoB,UAAa,mBAAmB,OACpD,0BAA0B,iCAAiC,YAAY,QAAQ;AAClF,UAAM,uBAAuB,mBAAmB;AAChD,UAAM,YAAY;AAChB,UAAI;AACF,cAAM,MAAM,IAAI,QAAa,UAAU;AACvC,YAAI,uBAAuB;AACzB,gBAAM,IAAI,UAAU,gCAAgC;AAAA,YAClD;AAAA,YACA,UAAU,YAAY;AAAA,YACtB,gBAAgB,kBAAkB;AAAA,YAClC,SAAS;AAAA,UACX,CAAC;AAAA,QACH;AACA,cAAM,IAAI,UAAU,wBAAwB,EAAE,UAAU,YAAY,UAAU,gBAAgB,SAAS,CAAC;AAAA,MAC1G,SAAS,OAAO;AACd,cAAM;AAAA,UACJ,EAAE,GAAG;AAAA,UACL;AAAA,YACE,QAAQ;AAAA,YACR,SAAS;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,YACA,UAAU,YAAY;AAAA,YACtB,gBAAgB,kBAAkB;AAAA,YAClC;AAAA,UACF;AAAA,QACF,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MAClB;AAAA,IACF,GAAG;AAAA,EACL,SAAS,OAAO;AACd,UAAM;AAAA,MACJ,EAAE,GAAG;AAAA,MACL;AAAA,QACE,QAAQ;AAAA,QACR,SAAS;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU,YAAY;AAAA,QACtB,gBAAgB,kBAAkB;AAAA,QAClC;AAAA,MACF;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;",
4
+ "sourcesContent": ["import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { sql } from 'kysely'\nimport { markDeleted } from '../lib/indexer'\nimport { applyCoverageAdjustments, createCoverageAdjustments } from '../lib/coverage'\nimport {\n loadQueryIndexRowScope,\n resolveQueryIndexRecordScope,\n resolveQueryIndexSourceMetadata,\n} from '../lib/subscriber-scope'\n\nexport const metadata = { event: 'query_index.delete_one', persistent: false }\n\n// Mirrors `shouldTriggerCoverageRefresh()` in `@open-mercato/shared/lib/data/engine.ts`\n// as a small local throttle so per-record deletes stop firing an unconditional, immediate\n// full recount. Note: this Map and the shared engine's own throttle Map are independent,\n// so a rare race right after a 5-minute window resets on both could double-fire once \u2014\n// harmless (the recompute is idempotent) and far cheaper than today's per-delete cost.\nconst DELETE_COVERAGE_THROTTLE_MS = 5 * 60 * 1000\nconst lastDeleteCoverageRefreshAt = new Map<string, number>()\n\nfunction shouldAllowDeleteCoverageRefresh(entityType: string, tenantId: string | null): boolean {\n if (!entityType) return false\n const key = `${entityType}|${tenantId ?? '__null__'}`\n const now = Date.now()\n const last = lastDeleteCoverageRefreshAt.get(key) ?? 0\n if (now - last < DELETE_COVERAGE_THROTTLE_MS) return false\n lastDeleteCoverageRefreshAt.set(key, now)\n return true\n}\n\nexport default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {\n // Forked EntityManager \u2014 this awaited subscriber runs synchronously on the request\n // `em`; isolating it prevents our queries/writes from resetting the originating CRUD\n // write's UnitOfWork and dropping its pending changes. See upsert_one.ts for detail.\n const baseEm = ctx.resolve<any>('em')\n const em = typeof baseEm?.fork === 'function' ? baseEm.fork() : baseEm\n const entityType = String(payload?.entityType || '')\n const recordId = String(payload?.recordId || '')\n if (!entityType || !recordId) return\n let organizationId: string | null = payload?.organizationId ?? null\n let tenantId: string | null = payload?.tenantId ?? null\n const coverageDelayMs = typeof payload?.coverageDelayMs === 'number' ? payload.coverageDelayMs : undefined\n try {\n const hasPayloadOrganizationId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'organizationId')\n const hasPayloadTenantId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'tenantId')\n const source = resolveQueryIndexSourceMetadata(em, entityType)\n const sourceScope = await loadQueryIndexRowScope(em, source, recordId)\n const resolvedScope = resolveQueryIndexRecordScope({\n payloadOrganizationId: payload?.organizationId,\n payloadTenantId: payload?.tenantId,\n hasPayloadOrganizationId,\n hasPayloadTenantId,\n sourceScope,\n })\n organizationId = resolvedScope.organizationId\n tenantId = resolvedScope.tenantId\n\n const { wasActive } = await markDeleted(em, { entityType, recordId, organizationId, tenantId })\n\n let baseDelta = 0\n let baseCheckSucceeded = false\n try {\n const db = (em as any).getKysely()\n let baseQuery = db\n .selectFrom(source.table as any)\n .select(['deleted_at' as any])\n .where('id' as any, '=', recordId)\n if (source.organizationColumn) {\n baseQuery = baseQuery.where(\n source.organizationColumn as any,\n organizationId === null ? 'is' : '=',\n organizationId as any,\n )\n }\n if (source.tenantColumn) {\n baseQuery = baseQuery.where(sql`${sql.ref(source.tenantColumn)} is not distinct from ${tenantId}`)\n }\n const row = await baseQuery.executeTakeFirst() as { deleted_at: Date | null } | undefined\n const baseMissing = !row\n const baseDeleted = baseMissing || (row && row.deleted_at != null)\n baseCheckSucceeded = true\n if (baseDeleted) baseDelta = -1\n } catch {}\n if (!baseCheckSucceeded) baseDelta = -1\n\n const baseDeltaOverride =\n typeof payload?.coverageBaseDelta === 'number' ? payload.coverageBaseDelta : undefined\n const indexDeltaOverride =\n typeof payload?.coverageIndexDelta === 'number' ? payload.coverageIndexDelta : undefined\n let effectiveBaseDelta = baseDeltaOverride ?? baseDelta\n let effectiveIndexDelta = indexDeltaOverride ?? (wasActive ? -1 : 0)\n\n if (!Number.isFinite(effectiveBaseDelta)) effectiveBaseDelta = 0\n if (!Number.isFinite(effectiveIndexDelta)) effectiveIndexDelta = 0\n\n if (effectiveBaseDelta !== 0 || effectiveIndexDelta !== 0) {\n const adjustments = createCoverageAdjustments({\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n baseDelta: effectiveBaseDelta,\n indexDelta: effectiveIndexDelta,\n })\n if (adjustments.length) {\n await applyCoverageAdjustments(em, adjustments)\n }\n }\n\n // The projection row + token removal above are synchronous (the data engine\n // awaits this subscriber) so list reads are consistent immediately. The coverage\n // recompute (a COUNT, run inline when delayMs is 0) and the fulltext delete are\n // secondary, so defer them fire-and-forget to keep write/bulk-delete latency bounded.\n const suppressCoverage = payload?.suppressCoverage === true\n const explicitDelayRequested = typeof payload?.coverageDelayMs === 'number'\n const shouldRefreshCoverage =\n !suppressCoverage &&\n (coverageDelayMs === undefined || coverageDelayMs >= 0) &&\n (explicitDelayRequested || shouldAllowDeleteCoverageRefresh(entityType, tenantId))\n const coverageRefreshDelay = coverageDelayMs ?? 0\n void (async () => {\n try {\n const bus = ctx.resolve<any>('eventBus')\n if (shouldRefreshCoverage) {\n await bus.emitEvent('query_index.coverage.refresh', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n delayMs: coverageRefreshDelay,\n })\n }\n await bus.emitEvent('search.delete_record', { entityId: entityType, recordId, organizationId, tenantId })\n } catch (error) {\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.delete_one:coverage_search',\n error,\n entityType,\n recordId,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n ).catch(() => {})\n }\n })()\n } catch (error) {\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.delete_one',\n error,\n entityType,\n recordId,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n )\n throw error\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,11 @@
1
1
  import { recordIndexerError } from "@open-mercato/shared/lib/indexers/error-log";
2
2
  import { upsertIndexRow, reindexSearchTokensForRecord } from "../lib/indexer.js";
3
3
  import { applyCoverageAdjustments, createCoverageAdjustments } from "../lib/coverage.js";
4
- import { loadQueryIndexRowScope, resolveQueryIndexRecordScope } from "../lib/subscriber-scope.js";
4
+ import {
5
+ loadQueryIndexRowScope,
6
+ resolveQueryIndexRecordScope,
7
+ resolveQueryIndexSourceMetadata
8
+ } from "../lib/subscriber-scope.js";
5
9
  const metadata = { event: "query_index.upsert_one", persistent: false };
6
10
  async function handle(payload, ctx) {
7
11
  const baseEm = ctx.resolve("em");
@@ -16,13 +20,14 @@ async function handle(payload, ctx) {
16
20
  try {
17
21
  const hasPayloadOrganizationId = Object.prototype.hasOwnProperty.call(payload ?? {}, "organizationId");
18
22
  const hasPayloadTenantId = Object.prototype.hasOwnProperty.call(payload ?? {}, "tenantId");
19
- const rowScope = await loadQueryIndexRowScope(em, entityType, recordId).catch(() => null);
23
+ const source = resolveQueryIndexSourceMetadata(em, entityType);
24
+ const sourceScope = await loadQueryIndexRowScope(em, source, recordId);
20
25
  const resolvedScope = resolveQueryIndexRecordScope({
21
26
  payloadOrganizationId: payload?.organizationId,
22
27
  payloadTenantId: payload?.tenantId,
23
28
  hasPayloadOrganizationId,
24
29
  hasPayloadTenantId,
25
- rowScope
30
+ sourceScope
26
31
  });
27
32
  organizationId = resolvedScope.organizationId;
28
33
  tenantId = resolvedScope.tenantId;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/subscribers/upsert_one.ts"],
4
- "sourcesContent": ["import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { upsertIndexRow, reindexSearchTokensForRecord } from '../lib/indexer'\nimport { applyCoverageAdjustments, createCoverageAdjustments } from '../lib/coverage'\nimport { loadQueryIndexRowScope, resolveQueryIndexRecordScope } from '../lib/subscriber-scope'\n\nexport const metadata = { event: 'query_index.upsert_one', persistent: false }\n\nexport default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {\n // Run index maintenance on a FORKED EntityManager (fresh identity map + UnitOfWork)\n // so it can never disturb the originating CRUD write's `em`. The data engine awaits\n // this emit for read-your-writes consistency, which means the subscriber runs\n // synchronously on the request `em`; sharing it would let our `em.find` / raw\n // `getKysely()` queries reset the caller's UoW change-tracking and silently drop the\n // caller's pending write (e.g. the deal's `setCustomFields` insert). The fork reads\n // the same committed DB rows via the shared connection but keeps its own UoW.\n const baseEm = ctx.resolve<any>('em')\n const em = typeof baseEm?.fork === 'function' ? baseEm.fork() : baseEm\n const entityType = String(payload?.entityType || '')\n const recordId = String(payload?.recordId || '')\n if (!entityType || !recordId) return\n let organizationId: string | null = payload?.organizationId ?? null\n let tenantId: string | null = payload?.tenantId ?? null\n const suppressCoverage = payload?.suppressCoverage === true\n const coverageDelayMs = typeof payload?.coverageDelayMs === 'number' ? payload.coverageDelayMs : undefined\n try {\n const hasPayloadOrganizationId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'organizationId')\n const hasPayloadTenantId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'tenantId')\n const rowScope = await loadQueryIndexRowScope(em, entityType, recordId).catch(() => null)\n const resolvedScope = resolveQueryIndexRecordScope({\n payloadOrganizationId: payload?.organizationId,\n payloadTenantId: payload?.tenantId,\n hasPayloadOrganizationId,\n hasPayloadTenantId,\n rowScope,\n })\n organizationId = resolvedScope.organizationId\n tenantId = resolvedScope.tenantId\n\n const searchTokenDoc = typeof payload?.searchTokenDoc === 'object' && payload.searchTokenDoc && !Array.isArray(payload.searchTokenDoc)\n ? (payload.searchTokenDoc as Record<string, unknown>)\n : null\n // Update the projection row synchronously so list reads (`customValues`) are\n // consistent the moment the write returns; defer the heavy search-token rebuild.\n const result = await upsertIndexRow(em, {\n entityType,\n recordId,\n organizationId,\n tenantId,\n searchTokenDoc,\n deferSearchTokens: true,\n })\n if (!suppressCoverage) {\n const doc = result.doc\n const isActive = !!doc && (doc.deleted_at == null || doc.deleted_at === null)\n let baseDelta: number | undefined =\n typeof payload?.coverageBaseDelta === 'number' ? payload.coverageBaseDelta : undefined\n let indexDelta: number | undefined =\n typeof payload?.coverageIndexDelta === 'number' ? payload.coverageIndexDelta : undefined\n const crudAction = typeof payload?.crudAction === 'string' ? payload.crudAction : undefined\n\n if (baseDelta === undefined) {\n if (result.revived) baseDelta = 1\n else if (crudAction === 'created') baseDelta = 1\n else baseDelta = 0\n }\n\n if (indexDelta === undefined) {\n if (isActive && (result.created || result.revived)) indexDelta = 1\n else indexDelta = 0\n }\n\n if (!isActive && baseDelta > 0) baseDelta = 0\n if (!isActive && indexDelta > 0) indexDelta = 0\n if (!Number.isFinite(baseDelta)) baseDelta = 0\n if (!Number.isFinite(indexDelta)) indexDelta = 0\n\n const adjustments = createCoverageAdjustments({\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n baseDelta,\n indexDelta,\n })\n if (adjustments.length) {\n await applyCoverageAdjustments(em, adjustments)\n }\n if (coverageDelayMs !== undefined && coverageDelayMs >= 0) {\n try {\n const bus = ctx.resolve<any>('eventBus')\n await bus.emitEvent('query_index.coverage.refresh', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n delayMs: coverageDelayMs,\n })\n } catch {}\n }\n }\n // Defer the heavy, eventually-consistent tail: search-token rebuild + vectorize +\n // fulltext indexing. The data engine awaits this subscriber for projection\n // consistency, so this work runs fire-and-forget to keep write latency bounded.\n const deferredScope = { entityType, recordId, organizationId, tenantId }\n const resolvedDoc = result.doc\n void (async () => {\n try {\n await reindexSearchTokensForRecord(em, { ...deferredScope, doc: resolvedDoc, searchTokenDoc })\n const bus = ctx.resolve<any>('eventBus')\n await bus.emitEvent('query_index.vectorize_one', deferredScope)\n await bus.emitEvent('search.index_record', { entityId: entityType, recordId, organizationId, tenantId })\n } catch (error) {\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.upsert_one:search_tokens',\n error,\n entityType,\n recordId,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n ).catch(() => {})\n }\n })()\n } catch (error) {\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.upsert_one',\n error,\n entityType,\n recordId,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n )\n throw error\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { upsertIndexRow, reindexSearchTokensForRecord } from '../lib/indexer'\nimport { applyCoverageAdjustments, createCoverageAdjustments } from '../lib/coverage'\nimport {\n loadQueryIndexRowScope,\n resolveQueryIndexRecordScope,\n resolveQueryIndexSourceMetadata,\n} from '../lib/subscriber-scope'\n\nexport const metadata = { event: 'query_index.upsert_one', persistent: false }\n\nexport default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {\n // Run index maintenance on a FORKED EntityManager (fresh identity map + UnitOfWork)\n // so it can never disturb the originating CRUD write's `em`. The data engine awaits\n // this emit for read-your-writes consistency, which means the subscriber runs\n // synchronously on the request `em`; sharing it would let our `em.find` / raw\n // `getKysely()` queries reset the caller's UoW change-tracking and silently drop the\n // caller's pending write (e.g. the deal's `setCustomFields` insert). The fork reads\n // the same committed DB rows via the shared connection but keeps its own UoW.\n const baseEm = ctx.resolve<any>('em')\n const em = typeof baseEm?.fork === 'function' ? baseEm.fork() : baseEm\n const entityType = String(payload?.entityType || '')\n const recordId = String(payload?.recordId || '')\n if (!entityType || !recordId) return\n let organizationId: string | null = payload?.organizationId ?? null\n let tenantId: string | null = payload?.tenantId ?? null\n const suppressCoverage = payload?.suppressCoverage === true\n const coverageDelayMs = typeof payload?.coverageDelayMs === 'number' ? payload.coverageDelayMs : undefined\n try {\n const hasPayloadOrganizationId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'organizationId')\n const hasPayloadTenantId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'tenantId')\n const source = resolveQueryIndexSourceMetadata(em, entityType)\n const sourceScope = await loadQueryIndexRowScope(em, source, recordId)\n const resolvedScope = resolveQueryIndexRecordScope({\n payloadOrganizationId: payload?.organizationId,\n payloadTenantId: payload?.tenantId,\n hasPayloadOrganizationId,\n hasPayloadTenantId,\n sourceScope,\n })\n organizationId = resolvedScope.organizationId\n tenantId = resolvedScope.tenantId\n\n const searchTokenDoc = typeof payload?.searchTokenDoc === 'object' && payload.searchTokenDoc && !Array.isArray(payload.searchTokenDoc)\n ? (payload.searchTokenDoc as Record<string, unknown>)\n : null\n // Update the projection row synchronously so list reads (`customValues`) are\n // consistent the moment the write returns; defer the heavy search-token rebuild.\n const result = await upsertIndexRow(em, {\n entityType,\n recordId,\n organizationId,\n tenantId,\n searchTokenDoc,\n deferSearchTokens: true,\n })\n if (!suppressCoverage) {\n const doc = result.doc\n const isActive = !!doc && (doc.deleted_at == null || doc.deleted_at === null)\n let baseDelta: number | undefined =\n typeof payload?.coverageBaseDelta === 'number' ? payload.coverageBaseDelta : undefined\n let indexDelta: number | undefined =\n typeof payload?.coverageIndexDelta === 'number' ? payload.coverageIndexDelta : undefined\n const crudAction = typeof payload?.crudAction === 'string' ? payload.crudAction : undefined\n\n if (baseDelta === undefined) {\n if (result.revived) baseDelta = 1\n else if (crudAction === 'created') baseDelta = 1\n else baseDelta = 0\n }\n\n if (indexDelta === undefined) {\n if (isActive && (result.created || result.revived)) indexDelta = 1\n else indexDelta = 0\n }\n\n if (!isActive && baseDelta > 0) baseDelta = 0\n if (!isActive && indexDelta > 0) indexDelta = 0\n if (!Number.isFinite(baseDelta)) baseDelta = 0\n if (!Number.isFinite(indexDelta)) indexDelta = 0\n\n const adjustments = createCoverageAdjustments({\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n baseDelta,\n indexDelta,\n })\n if (adjustments.length) {\n await applyCoverageAdjustments(em, adjustments)\n }\n if (coverageDelayMs !== undefined && coverageDelayMs >= 0) {\n try {\n const bus = ctx.resolve<any>('eventBus')\n await bus.emitEvent('query_index.coverage.refresh', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n delayMs: coverageDelayMs,\n })\n } catch {}\n }\n }\n // Defer the heavy, eventually-consistent tail: search-token rebuild + vectorize +\n // fulltext indexing. The data engine awaits this subscriber for projection\n // consistency, so this work runs fire-and-forget to keep write latency bounded.\n const deferredScope = { entityType, recordId, organizationId, tenantId }\n const resolvedDoc = result.doc\n void (async () => {\n try {\n await reindexSearchTokensForRecord(em, { ...deferredScope, doc: resolvedDoc, searchTokenDoc })\n const bus = ctx.resolve<any>('eventBus')\n await bus.emitEvent('query_index.vectorize_one', deferredScope)\n await bus.emitEvent('search.index_record', { entityId: entityType, recordId, organizationId, tenantId })\n } catch (error) {\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.upsert_one:search_tokens',\n error,\n entityType,\n recordId,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n ).catch(() => {})\n }\n })()\n } catch (error) {\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.upsert_one',\n error,\n entityType,\n recordId,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n )\n throw error\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,6 +7,7 @@ import { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from "@op
7
7
  import { WorkflowDefinition } from "../../../../data/entities.js";
8
8
  import { serializeWorkflowDefinition } from "../../serialize.js";
9
9
  import { getCodeWorkflow } from "../../../../lib/code-registry.js";
10
+ import { invalidateTriggerCache } from "../../../../lib/event-trigger-service.js";
10
11
  import { createLogger } from "@open-mercato/shared/lib/logger";
11
12
  const logger = createLogger("workflows");
12
13
  const metadata = {
@@ -88,6 +89,7 @@ async function POST(request, context) {
88
89
  await em.flush();
89
90
  saved = override;
90
91
  }
92
+ if (saved.tenantId) invalidateTriggerCache(saved.tenantId, saved.organizationId ?? void 0);
91
93
  if (guardResult?.shouldRunAfterSuccess) {
92
94
  await runCrudMutationGuardAfterSuccess(container, {
93
95
  tenantId: tenantId ?? "",