@open-mercato/search 0.5.1-develop.2691.d8a0934b37 → 0.5.1-develop.2694.732417c5ec

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 (46) hide show
  1. package/dist/di.js +9 -9
  2. package/dist/di.js.map +2 -2
  3. package/dist/lib/presenter-enricher.js +14 -14
  4. package/dist/lib/presenter-enricher.js.map +2 -2
  5. package/dist/modules/search/api/embeddings/reindex/cancel/route.js +2 -2
  6. package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +2 -2
  7. package/dist/modules/search/api/embeddings/reindex/route.js +3 -3
  8. package/dist/modules/search/api/embeddings/reindex/route.js.map +2 -2
  9. package/dist/modules/search/api/reindex/cancel/route.js +2 -2
  10. package/dist/modules/search/api/reindex/cancel/route.js.map +2 -2
  11. package/dist/modules/search/api/reindex/route.js +4 -4
  12. package/dist/modules/search/api/reindex/route.js.map +2 -2
  13. package/dist/modules/search/api/settings/route.js +3 -3
  14. package/dist/modules/search/api/settings/route.js.map +2 -2
  15. package/dist/modules/search/lib/reindex-lock.js +20 -17
  16. package/dist/modules/search/lib/reindex-lock.js.map +2 -2
  17. package/dist/modules/search/subscribers/fulltext_upsert.js +2 -2
  18. package/dist/modules/search/subscribers/fulltext_upsert.js.map +2 -2
  19. package/dist/modules/search/subscribers/vector_delete.js +2 -2
  20. package/dist/modules/search/subscribers/vector_delete.js.map +2 -2
  21. package/dist/modules/search/subscribers/vector_upsert.js +2 -2
  22. package/dist/modules/search/subscribers/vector_upsert.js.map +2 -2
  23. package/dist/modules/search/workers/fulltext-index.worker.js +7 -7
  24. package/dist/modules/search/workers/fulltext-index.worker.js.map +2 -2
  25. package/dist/modules/search/workers/vector-index.worker.js +7 -7
  26. package/dist/modules/search/workers/vector-index.worker.js.map +2 -2
  27. package/dist/strategies/token.strategy.js +15 -10
  28. package/dist/strategies/token.strategy.js.map +2 -2
  29. package/jest.config.cjs +4 -2
  30. package/package.json +4 -4
  31. package/src/__tests__/presenter-enricher.test.ts +17 -60
  32. package/src/__tests__/workers.test.ts +20 -21
  33. package/src/di.ts +22 -21
  34. package/src/lib/presenter-enricher.ts +21 -20
  35. package/src/modules/search/api/embeddings/reindex/cancel/route.ts +4 -3
  36. package/src/modules/search/api/embeddings/reindex/route.ts +5 -4
  37. package/src/modules/search/api/reindex/cancel/route.ts +4 -3
  38. package/src/modules/search/api/reindex/route.ts +5 -5
  39. package/src/modules/search/api/settings/route.ts +5 -4
  40. package/src/modules/search/lib/reindex-lock.ts +50 -32
  41. package/src/modules/search/subscribers/fulltext_upsert.ts +6 -2
  42. package/src/modules/search/subscribers/vector_delete.ts +6 -2
  43. package/src/modules/search/subscribers/vector_upsert.ts +6 -2
  44. package/src/modules/search/workers/fulltext-index.worker.ts +10 -9
  45. package/src/modules/search/workers/vector-index.worker.ts +10 -9
  46. package/src/strategies/token.strategy.ts +25 -19
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/search/api/reindex/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { SearchStrategy } from '@open-mercato/shared/modules/search'\nimport type { SearchIndexer } from '@open-mercato/search/indexer'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Knex } from 'knex'\nimport { searchDebug, searchError } from '../../../../lib/debug'\nimport {\n acquireReindexLock,\n clearReindexLock,\n getReindexLockStatus,\n} from '../../lib/reindex-lock'\nimport {\n completeReindexProgress,\n ensureReindexProgressJob,\n failReindexProgress,\n} from '../../lib/reindex-progress'\nimport { reindexOpenApi } from '../openapi'\n\n/** Strategy with optional stats support */\ntype StrategyWithStats = SearchStrategy & {\n getIndexStats?: (tenantId: string) => Promise<Record<string, unknown> | null>\n clearIndex?: (tenantId: string) => Promise<void>\n recreateIndex?: (tenantId: string) => Promise<void>\n}\n\n/** Collect stats from all strategies that support it */\nasync function collectStrategyStats(\n strategies: StrategyWithStats[],\n tenantId: string\n): Promise<Record<string, Record<string, unknown> | null>> {\n const stats: Record<string, Record<string, unknown> | null> = {}\n for (const strategy of strategies) {\n if (typeof strategy.getIndexStats === 'function') {\n try {\n const isAvailable = await strategy.isAvailable()\n if (isAvailable) {\n stats[strategy.id] = await strategy.getIndexStats(tenantId)\n }\n } catch {\n // Skip strategy if stats collection fails\n }\n }\n }\n return stats\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['search.reindex'] },\n}\n\ntype ReindexAction = 'clear' | 'recreate' | 'reindex'\n\nconst toJson = (payload: Record<string, unknown>, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return await unauthorized()\n }\n\n // Capture tenantId as non-null for TypeScript (we checked above)\n const tenantId = auth.tenantId\n\n let payload: { action?: ReindexAction; entityId?: string; useQueue?: boolean } = {}\n try {\n payload = await req.json()\n } catch {\n // Default to reindex\n }\n\n const action: ReindexAction =\n payload.action === 'clear' ? 'clear' :\n payload.action === 'recreate' ? 'recreate' : 'reindex'\n const entityId = typeof payload.entityId === 'string' ? payload.entityId : undefined\n // Use queue by default (requires queue workers to be running), can be disabled with useQueue: false\n const useQueue = payload.useQueue !== false\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const progressService = container.resolve('progressService') as ProgressService\n const knex = (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()\n\n // Check if another fulltext reindex operation is already in progress\n const existingLock = await getReindexLockStatus(knex, tenantId, { type: 'fulltext' })\n if (existingLock) {\n const startedAt = new Date(existingLock.startedAt)\n return NextResponse.json(\n {\n error: t('search.api.errors.reindexInProgress', 'A reindex operation is already in progress'),\n lock: {\n type: existingLock.type,\n action: existingLock.action,\n startedAt: existingLock.startedAt,\n elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),\n processedCount: existingLock.processedCount,\n totalCount: existingLock.totalCount,\n },\n },\n { status: 409 }\n )\n }\n\n // Acquire lock before starting the operation\n const { acquired: lockAcquired } = await acquireReindexLock(knex, {\n type: 'fulltext',\n action,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n })\n\n if (!lockAcquired) {\n return NextResponse.json(\n { error: t('search.api.errors.lockFailed', 'Failed to acquire reindex lock') },\n { status: 409 }\n )\n }\n\n try {\n // Get all search strategies\n const searchStrategies = (container.resolve('searchStrategies') as StrategyWithStats[] | undefined) ?? []\n\n // Find a strategy that supports index management (clear/recreate)\n const indexableStrategy = searchStrategies.find(\n (s) => typeof s.clearIndex === 'function' || typeof s.recreateIndex === 'function'\n )\n\n if (!indexableStrategy) {\n return toJson(\n { error: t('search.api.errors.noIndexableStrategy', 'No indexable search strategy is configured') },\n { status: 503 }\n )\n }\n\n // Check if strategy is available\n const isAvailable = await indexableStrategy.isAvailable()\n if (!isAvailable) {\n return toJson(\n { error: t('search.api.errors.strategyUnavailable', 'Search strategy is not available') },\n { status: 503 }\n )\n }\n\n // Perform the requested action\n if (action === 'reindex') {\n // Full reindex: recreate index and re-index all data\n const searchIndexer = container.resolve('searchIndexer') as SearchIndexer | undefined\n if (!searchIndexer) {\n return toJson(\n { error: t('search.api.errors.indexerUnavailable', 'Search indexer is not available') },\n { status: 503 }\n )\n }\n\n let result\n const orgId = typeof auth.orgId === 'string' ? auth.orgId : null\n\n // Debug: List enabled entities\n const enabledEntities = searchIndexer.listEnabledEntities()\n searchDebug('search.reindex', 'Starting reindex', {\n tenantId: tenantId,\n orgId,\n enabledEntities,\n entityId: entityId ?? 'all',\n useQueue,\n })\n\n // Log reindex started\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: entityId\n ? `Starting Meilisearch reindex for ${entityId}`\n : `Starting Meilisearch reindex for all entities (${enabledEntities.join(', ')})`,\n entityType: entityId ?? null,\n tenantId: tenantId,\n organizationId: orgId,\n details: { enabledEntities, useQueue },\n },\n )\n\n if (entityId) {\n // Reindex specific entity\n result = await searchIndexer.reindexEntityToFulltext({\n entityId: entityId as EntityId,\n tenantId: tenantId,\n organizationId: orgId,\n recreateIndex: true,\n useQueue,\n onProgress: (progress) => {\n searchDebug('search.reindex', 'Progress', progress)\n // Note: Heartbeat is updated by workers during job processing, not during enqueueing\n },\n })\n searchDebug('search.reindex', 'Reindexed entity to Meilisearch', {\n entityId,\n tenantId: tenantId,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n errors: result.errors,\n })\n\n // Log to indexer status logs\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: useQueue\n ? `Enqueued ${result.jobsEnqueued ?? 0} jobs for Meilisearch reindex of ${entityId}`\n : `Reindexed ${result.recordsIndexed} records to Meilisearch for ${entityId}`,\n entityType: entityId,\n tenantId: tenantId,\n organizationId: orgId,\n details: {\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n useQueue,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n },\n )\n\n // Log any batch errors to error logs\n for (const err of result.errors) {\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error: new Error(err.error),\n entityType: err.entityId,\n tenantId: tenantId,\n organizationId: orgId,\n payload: { action, useQueue },\n },\n )\n }\n } else {\n // Reindex all entities\n result = await searchIndexer.reindexAllToFulltext({\n tenantId: tenantId,\n organizationId: orgId,\n recreateIndex: true,\n useQueue,\n onProgress: (progress) => {\n searchDebug('search.reindex', 'Progress', progress)\n // Note: Heartbeat is updated by workers during job processing, not during enqueueing\n },\n })\n searchDebug('search.reindex', 'Reindexed all entities to Meilisearch', {\n tenantId: tenantId,\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n errors: result.errors,\n })\n\n // Log to indexer status logs\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: useQueue\n ? `Enqueued ${result.jobsEnqueued ?? 0} jobs for Meilisearch reindex of all entities`\n : `Reindexed ${result.recordsIndexed} records to Meilisearch for ${result.entitiesProcessed} entities`,\n tenantId: tenantId,\n organizationId: orgId,\n details: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n useQueue,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n },\n )\n\n // Log any batch errors to error logs\n for (const err of result.errors) {\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error: new Error(err.error),\n entityType: err.entityId,\n tenantId: tenantId,\n organizationId: orgId,\n payload: { action, useQueue },\n },\n )\n }\n }\n\n await ensureReindexProgressJob({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub ?? null,\n totalCount: result.recordsIndexed,\n description: entityId\n ? `Reindex ${entityId} (${useQueue ? 'queued' : 'sync'})`\n : `Reindex all entities (${useQueue ? 'queued' : 'sync'})`,\n })\n if (!useQueue) {\n await completeReindexProgress({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n resultSummary: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued ?? 0,\n errors: result.errors.length,\n },\n })\n }\n\n // Get updated stats from all strategies\n const stats = await collectStrategyStats(searchStrategies, tenantId)\n\n return toJson({\n ok: result.success,\n action,\n entityId: entityId ?? null,\n useQueue,\n result: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued ?? 0,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n stats,\n })\n } else if (entityId) {\n // Purge specific entity\n await indexableStrategy.purge?.(entityId as EntityId, tenantId)\n searchDebug('search.reindex', 'Purged entity', { strategyId: indexableStrategy.id, entityId, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: `Purged entity ${entityId} from Meilisearch`,\n entityType: entityId,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n } else if (action === 'clear') {\n // Clear all documents but keep index\n if (indexableStrategy.clearIndex) {\n await indexableStrategy.clearIndex(tenantId)\n searchDebug('search.reindex', 'Cleared index', { strategyId: indexableStrategy.id, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: 'Cleared all documents from Meilisearch index',\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n }\n } else {\n // Recreate the entire index\n if (indexableStrategy.recreateIndex) {\n await indexableStrategy.recreateIndex(tenantId)\n searchDebug('search.reindex', 'Recreated index', { strategyId: indexableStrategy.id, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: 'Recreated Meilisearch index',\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n }\n }\n\n // Get updated stats from all strategies\n const stats = await collectStrategyStats(searchStrategies, tenantId)\n\n return toJson({\n ok: true,\n action,\n entityId: entityId ?? null,\n stats,\n })\n } catch (error: unknown) {\n // Log full error details server-side only\n searchError('search.reindex', 'Failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n tenantId: tenantId,\n })\n\n // Record error to indexer error logs\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error,\n entityType: entityId ?? null,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n payload: { action, entityId, useQueue },\n },\n )\n\n await failReindexProgress({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n errorMessage: error instanceof Error ? error.message : 'Fulltext reindex failed',\n })\n\n // Return generic message to client - don't expose internal error details\n return toJson(\n { error: t('search.api.errors.reindexFailed', 'Reindex operation failed. Please try again or contact support.') },\n { status: 500 }\n )\n } finally {\n // Only clear lock immediately if NOT using queue mode\n // When using queue mode, workers update heartbeat and stale detection handles cleanup\n if (!useQueue) {\n await clearReindexLock(knex, tenantId, 'fulltext', auth.orgId ?? null)\n }\n\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = reindexOpenApi\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { SearchStrategy } from '@open-mercato/shared/modules/search'\nimport type { SearchIndexer } from '@open-mercato/search/indexer'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Kysely } from 'kysely'\nimport { searchDebug, searchError } from '../../../../lib/debug'\nimport {\n acquireReindexLock,\n clearReindexLock,\n getReindexLockStatus,\n} from '../../lib/reindex-lock'\nimport {\n completeReindexProgress,\n ensureReindexProgressJob,\n failReindexProgress,\n} from '../../lib/reindex-progress'\nimport { reindexOpenApi } from '../openapi'\n\n/** Strategy with optional stats support */\ntype StrategyWithStats = SearchStrategy & {\n getIndexStats?: (tenantId: string) => Promise<Record<string, unknown> | null>\n clearIndex?: (tenantId: string) => Promise<void>\n recreateIndex?: (tenantId: string) => Promise<void>\n}\n\n/** Collect stats from all strategies that support it */\nasync function collectStrategyStats(\n strategies: StrategyWithStats[],\n tenantId: string\n): Promise<Record<string, Record<string, unknown> | null>> {\n const stats: Record<string, Record<string, unknown> | null> = {}\n for (const strategy of strategies) {\n if (typeof strategy.getIndexStats === 'function') {\n try {\n const isAvailable = await strategy.isAvailable()\n if (isAvailable) {\n stats[strategy.id] = await strategy.getIndexStats(tenantId)\n }\n } catch {\n // Skip strategy if stats collection fails\n }\n }\n }\n return stats\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['search.reindex'] },\n}\n\ntype ReindexAction = 'clear' | 'recreate' | 'reindex'\n\nconst toJson = (payload: Record<string, unknown>, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return await unauthorized()\n }\n\n // Capture tenantId as non-null for TypeScript (we checked above)\n const tenantId = auth.tenantId\n\n let payload: { action?: ReindexAction; entityId?: string; useQueue?: boolean } = {}\n try {\n payload = await req.json()\n } catch {\n // Default to reindex\n }\n\n const action: ReindexAction =\n payload.action === 'clear' ? 'clear' :\n payload.action === 'recreate' ? 'recreate' : 'reindex'\n const entityId = typeof payload.entityId === 'string' ? payload.entityId : undefined\n // Use queue by default (requires queue workers to be running), can be disabled with useQueue: false\n const useQueue = payload.useQueue !== false\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const progressService = container.resolve('progressService') as ProgressService\n const db = (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n\n // Check if another fulltext reindex operation is already in progress\n const existingLock = await getReindexLockStatus(db, tenantId, { type: 'fulltext' })\n if (existingLock) {\n const startedAt = new Date(existingLock.startedAt)\n return NextResponse.json(\n {\n error: t('search.api.errors.reindexInProgress', 'A reindex operation is already in progress'),\n lock: {\n type: existingLock.type,\n action: existingLock.action,\n startedAt: existingLock.startedAt,\n elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),\n processedCount: existingLock.processedCount,\n totalCount: existingLock.totalCount,\n },\n },\n { status: 409 }\n )\n }\n\n // Acquire lock before starting the operation\n const { acquired: lockAcquired } = await acquireReindexLock(db, {\n type: 'fulltext',\n action,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n })\n\n if (!lockAcquired) {\n return NextResponse.json(\n { error: t('search.api.errors.lockFailed', 'Failed to acquire reindex lock') },\n { status: 409 }\n )\n }\n\n try {\n // Get all search strategies\n const searchStrategies = (container.resolve('searchStrategies') as StrategyWithStats[] | undefined) ?? []\n\n // Find a strategy that supports index management (clear/recreate)\n const indexableStrategy = searchStrategies.find(\n (s) => typeof s.clearIndex === 'function' || typeof s.recreateIndex === 'function'\n )\n\n if (!indexableStrategy) {\n return toJson(\n { error: t('search.api.errors.noIndexableStrategy', 'No indexable search strategy is configured') },\n { status: 503 }\n )\n }\n\n // Check if strategy is available\n const isAvailable = await indexableStrategy.isAvailable()\n if (!isAvailable) {\n return toJson(\n { error: t('search.api.errors.strategyUnavailable', 'Search strategy is not available') },\n { status: 503 }\n )\n }\n\n // Perform the requested action\n if (action === 'reindex') {\n // Full reindex: recreate index and re-index all data\n const searchIndexer = container.resolve('searchIndexer') as SearchIndexer | undefined\n if (!searchIndexer) {\n return toJson(\n { error: t('search.api.errors.indexerUnavailable', 'Search indexer is not available') },\n { status: 503 }\n )\n }\n\n let result\n const orgId = typeof auth.orgId === 'string' ? auth.orgId : null\n\n // Debug: List enabled entities\n const enabledEntities = searchIndexer.listEnabledEntities()\n searchDebug('search.reindex', 'Starting reindex', {\n tenantId: tenantId,\n orgId,\n enabledEntities,\n entityId: entityId ?? 'all',\n useQueue,\n })\n\n // Log reindex started\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: entityId\n ? `Starting Meilisearch reindex for ${entityId}`\n : `Starting Meilisearch reindex for all entities (${enabledEntities.join(', ')})`,\n entityType: entityId ?? null,\n tenantId: tenantId,\n organizationId: orgId,\n details: { enabledEntities, useQueue },\n },\n )\n\n if (entityId) {\n // Reindex specific entity\n result = await searchIndexer.reindexEntityToFulltext({\n entityId: entityId as EntityId,\n tenantId: tenantId,\n organizationId: orgId,\n recreateIndex: true,\n useQueue,\n onProgress: (progress) => {\n searchDebug('search.reindex', 'Progress', progress)\n // Note: Heartbeat is updated by workers during job processing, not during enqueueing\n },\n })\n searchDebug('search.reindex', 'Reindexed entity to Meilisearch', {\n entityId,\n tenantId: tenantId,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n errors: result.errors,\n })\n\n // Log to indexer status logs\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: useQueue\n ? `Enqueued ${result.jobsEnqueued ?? 0} jobs for Meilisearch reindex of ${entityId}`\n : `Reindexed ${result.recordsIndexed} records to Meilisearch for ${entityId}`,\n entityType: entityId,\n tenantId: tenantId,\n organizationId: orgId,\n details: {\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n useQueue,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n },\n )\n\n // Log any batch errors to error logs\n for (const err of result.errors) {\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error: new Error(err.error),\n entityType: err.entityId,\n tenantId: tenantId,\n organizationId: orgId,\n payload: { action, useQueue },\n },\n )\n }\n } else {\n // Reindex all entities\n result = await searchIndexer.reindexAllToFulltext({\n tenantId: tenantId,\n organizationId: orgId,\n recreateIndex: true,\n useQueue,\n onProgress: (progress) => {\n searchDebug('search.reindex', 'Progress', progress)\n // Note: Heartbeat is updated by workers during job processing, not during enqueueing\n },\n })\n searchDebug('search.reindex', 'Reindexed all entities to Meilisearch', {\n tenantId: tenantId,\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n errors: result.errors,\n })\n\n // Log to indexer status logs\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: useQueue\n ? `Enqueued ${result.jobsEnqueued ?? 0} jobs for Meilisearch reindex of all entities`\n : `Reindexed ${result.recordsIndexed} records to Meilisearch for ${result.entitiesProcessed} entities`,\n tenantId: tenantId,\n organizationId: orgId,\n details: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n useQueue,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n },\n )\n\n // Log any batch errors to error logs\n for (const err of result.errors) {\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error: new Error(err.error),\n entityType: err.entityId,\n tenantId: tenantId,\n organizationId: orgId,\n payload: { action, useQueue },\n },\n )\n }\n }\n\n await ensureReindexProgressJob({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub ?? null,\n totalCount: result.recordsIndexed,\n description: entityId\n ? `Reindex ${entityId} (${useQueue ? 'queued' : 'sync'})`\n : `Reindex all entities (${useQueue ? 'queued' : 'sync'})`,\n })\n if (!useQueue) {\n await completeReindexProgress({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n resultSummary: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued ?? 0,\n errors: result.errors.length,\n },\n })\n }\n\n // Get updated stats from all strategies\n const stats = await collectStrategyStats(searchStrategies, tenantId)\n\n return toJson({\n ok: result.success,\n action,\n entityId: entityId ?? null,\n useQueue,\n result: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued ?? 0,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n stats,\n })\n } else if (entityId) {\n // Purge specific entity\n await indexableStrategy.purge?.(entityId as EntityId, tenantId)\n searchDebug('search.reindex', 'Purged entity', { strategyId: indexableStrategy.id, entityId, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: `Purged entity ${entityId} from Meilisearch`,\n entityType: entityId,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n } else if (action === 'clear') {\n // Clear all documents but keep index\n if (indexableStrategy.clearIndex) {\n await indexableStrategy.clearIndex(tenantId)\n searchDebug('search.reindex', 'Cleared index', { strategyId: indexableStrategy.id, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: 'Cleared all documents from Meilisearch index',\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n }\n } else {\n // Recreate the entire index\n if (indexableStrategy.recreateIndex) {\n await indexableStrategy.recreateIndex(tenantId)\n searchDebug('search.reindex', 'Recreated index', { strategyId: indexableStrategy.id, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: 'Recreated Meilisearch index',\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n }\n }\n\n // Get updated stats from all strategies\n const stats = await collectStrategyStats(searchStrategies, tenantId)\n\n return toJson({\n ok: true,\n action,\n entityId: entityId ?? null,\n stats,\n })\n } catch (error: unknown) {\n // Log full error details server-side only\n searchError('search.reindex', 'Failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n tenantId: tenantId,\n })\n\n // Record error to indexer error logs\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error,\n entityType: entityId ?? null,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n payload: { action, entityId, useQueue },\n },\n )\n\n await failReindexProgress({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n errorMessage: error instanceof Error ? error.message : 'Fulltext reindex failed',\n })\n\n // Return generic message to client - don't expose internal error details\n return toJson(\n { error: t('search.api.errors.reindexFailed', 'Reindex operation failed. Please try again or contact support.') },\n { status: 500 }\n )\n } finally {\n // Only clear lock immediately if NOT using queue mode\n // When using queue mode, workers update heartbeat and stale detection handles cleanup\n if (!useQueue) {\n await clearReindexLock(db, tenantId, 'fulltext', auth.orgId ?? null)\n }\n\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = reindexOpenApi\n"],
5
+ "mappings": "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",
6
6
  "names": ["stats"]
7
7
  }
@@ -65,8 +65,8 @@ async function GET(req) {
65
65
  let vectorReindexLock = null;
66
66
  if (auth.tenantId) {
67
67
  const em = container.resolve("em");
68
- const knex = em.getConnection().getKnex();
69
- const fulltextLockStatus = await getReindexLockStatus(knex, auth.tenantId, { type: "fulltext" });
68
+ const db = em.getKysely();
69
+ const fulltextLockStatus = await getReindexLockStatus(db, auth.tenantId, { type: "fulltext" });
70
70
  if (fulltextLockStatus) {
71
71
  const startedAt = new Date(fulltextLockStatus.startedAt);
72
72
  fulltextReindexLock = {
@@ -78,7 +78,7 @@ async function GET(req) {
78
78
  totalCount: fulltextLockStatus.totalCount
79
79
  };
80
80
  }
81
- const vectorLockStatus = await getReindexLockStatus(knex, auth.tenantId, { type: "vector" });
81
+ const vectorLockStatus = await getReindexLockStatus(db, auth.tenantId, { type: "vector" });
82
82
  if (vectorLockStatus) {
83
83
  const startedAt = new Date(vectorLockStatus.startedAt);
84
84
  vectorReindexLock = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/search/api/settings/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { SearchService } from '@open-mercato/search'\nimport type { FullTextSearchStrategy } from '@open-mercato/search/strategies'\nimport type { Knex } from 'knex'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getReindexLockStatus } from '../../lib/reindex-lock'\nimport { settingsOpenApi } from '../openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.view'] },\n}\n\ntype StrategyStatus = {\n id: string\n name: string\n priority: number\n available: boolean\n}\n\ntype FulltextStats = {\n numberOfDocuments: number\n isIndexing: boolean\n fieldDistribution: Record<string, number>\n}\n\ntype ReindexLock = {\n type: 'fulltext' | 'vector'\n action: string\n startedAt: string\n elapsedMinutes: number\n processedCount?: number | null\n totalCount?: number | null\n}\n\ntype SearchSettings = {\n strategies: StrategyStatus[]\n fulltextConfigured: boolean\n fulltextStats: FulltextStats | null\n vectorConfigured: boolean\n tokensEnabled: boolean\n defaultStrategies: string[]\n /** @deprecated Use fulltextReindexLock or vectorReindexLock instead */\n reindexLock: ReindexLock | null\n fulltextReindexLock: ReindexLock | null\n vectorReindexLock: ReindexLock | null\n}\n\ntype SettingsResponse = {\n settings: SearchSettings\n}\n\nconst toJson = (payload: SettingsResponse, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n const container = await createRequestContainer()\n try {\n const strategies: StrategyStatus[] = []\n let defaultStrategies: string[] = []\n let fulltextStats: FulltextStats | null = null\n\n try {\n const searchService = container.resolve('searchService') as SearchService | undefined\n const searchStrategies = container.resolve('searchStrategies') as unknown[] | undefined\n\n if (searchStrategies) {\n for (const strategy of searchStrategies) {\n const s = strategy as { id?: string; name?: string; priority?: number; isAvailable?: () => Promise<boolean> }\n const available = await s.isAvailable?.() ?? true\n strategies.push({\n id: s.id ?? 'unknown',\n name: s.name ?? s.id ?? 'unknown',\n priority: s.priority ?? 0,\n available,\n })\n }\n\n // Get fulltext stats if available and tenant is set\n if (auth.tenantId) {\n const fulltextStrategy = searchStrategies.find(\n (s: unknown) => (s as { id?: string })?.id === 'fulltext'\n ) as FullTextSearchStrategy | undefined\n\n if (fulltextStrategy) {\n try {\n const stats = await fulltextStrategy.getIndexStats(auth.tenantId)\n if (stats) {\n fulltextStats = stats\n }\n } catch {\n // Stats not available\n }\n }\n }\n }\n\n if (searchService) {\n defaultStrategies = searchService.getDefaultStrategies?.() ?? []\n }\n } catch {\n // Search service may not be available\n }\n\n const fulltextConfigured = Boolean(\n process.env.MEILISEARCH_HOST && process.env.MEILISEARCH_HOST.trim().length > 0\n )\n\n const vectorConfigured = Boolean(\n process.env.OPENAI_API_KEY ||\n process.env.GOOGLE_GENERATIVE_AI_API_KEY ||\n process.env.MISTRAL_API_KEY ||\n process.env.COHERE_API_KEY ||\n process.env.AWS_ACCESS_KEY_ID ||\n process.env.OLLAMA_BASE_URL\n )\n\n const tokensEnabled = process.env.OM_SEARCH_ENABLED !== 'false'\n\n // Check for active reindex locks with heartbeat-based stale detection\n let fulltextReindexLock: ReindexLock | null = null\n let vectorReindexLock: ReindexLock | null = null\n\n if (auth.tenantId) {\n const em = container.resolve('em') as EntityManager\n const knex = (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()\n\n // Check fulltext lock (auto-cleans stale locks based on heartbeat)\n const fulltextLockStatus = await getReindexLockStatus(knex, auth.tenantId, { type: 'fulltext' })\n if (fulltextLockStatus) {\n const startedAt = new Date(fulltextLockStatus.startedAt)\n fulltextReindexLock = {\n type: 'fulltext',\n action: fulltextLockStatus.action,\n startedAt: fulltextLockStatus.startedAt,\n elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),\n processedCount: fulltextLockStatus.processedCount,\n totalCount: fulltextLockStatus.totalCount,\n }\n }\n\n // Check vector lock (auto-cleans stale locks based on heartbeat)\n const vectorLockStatus = await getReindexLockStatus(knex, auth.tenantId, { type: 'vector' })\n if (vectorLockStatus) {\n const startedAt = new Date(vectorLockStatus.startedAt)\n vectorReindexLock = {\n type: 'vector',\n action: vectorLockStatus.action,\n startedAt: vectorLockStatus.startedAt,\n elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),\n processedCount: vectorLockStatus.processedCount,\n totalCount: vectorLockStatus.totalCount,\n }\n }\n }\n\n // Keep deprecated reindexLock for backwards compatibility (prefer fulltext if both are active)\n const reindexLock = fulltextReindexLock ?? vectorReindexLock\n\n return toJson({\n settings: {\n strategies,\n fulltextConfigured,\n fulltextStats,\n vectorConfigured,\n tokensEnabled,\n defaultStrategies,\n reindexLock,\n fulltextReindexLock,\n vectorReindexLock,\n },\n })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = settingsOpenApi\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { SearchService } from '@open-mercato/search'\nimport type { FullTextSearchStrategy } from '@open-mercato/search/strategies'\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Kysely } from 'kysely'\nimport { getReindexLockStatus } from '../../lib/reindex-lock'\nimport { settingsOpenApi } from '../openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.view'] },\n}\n\ntype StrategyStatus = {\n id: string\n name: string\n priority: number\n available: boolean\n}\n\ntype FulltextStats = {\n numberOfDocuments: number\n isIndexing: boolean\n fieldDistribution: Record<string, number>\n}\n\ntype ReindexLock = {\n type: 'fulltext' | 'vector'\n action: string\n startedAt: string\n elapsedMinutes: number\n processedCount?: number | null\n totalCount?: number | null\n}\n\ntype SearchSettings = {\n strategies: StrategyStatus[]\n fulltextConfigured: boolean\n fulltextStats: FulltextStats | null\n vectorConfigured: boolean\n tokensEnabled: boolean\n defaultStrategies: string[]\n /** @deprecated Use fulltextReindexLock or vectorReindexLock instead */\n reindexLock: ReindexLock | null\n fulltextReindexLock: ReindexLock | null\n vectorReindexLock: ReindexLock | null\n}\n\ntype SettingsResponse = {\n settings: SearchSettings\n}\n\nconst toJson = (payload: SettingsResponse, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n const container = await createRequestContainer()\n try {\n const strategies: StrategyStatus[] = []\n let defaultStrategies: string[] = []\n let fulltextStats: FulltextStats | null = null\n\n try {\n const searchService = container.resolve('searchService') as SearchService | undefined\n const searchStrategies = container.resolve('searchStrategies') as unknown[] | undefined\n\n if (searchStrategies) {\n for (const strategy of searchStrategies) {\n const s = strategy as { id?: string; name?: string; priority?: number; isAvailable?: () => Promise<boolean> }\n const available = await s.isAvailable?.() ?? true\n strategies.push({\n id: s.id ?? 'unknown',\n name: s.name ?? s.id ?? 'unknown',\n priority: s.priority ?? 0,\n available,\n })\n }\n\n // Get fulltext stats if available and tenant is set\n if (auth.tenantId) {\n const fulltextStrategy = searchStrategies.find(\n (s: unknown) => (s as { id?: string })?.id === 'fulltext'\n ) as FullTextSearchStrategy | undefined\n\n if (fulltextStrategy) {\n try {\n const stats = await fulltextStrategy.getIndexStats(auth.tenantId)\n if (stats) {\n fulltextStats = stats\n }\n } catch {\n // Stats not available\n }\n }\n }\n }\n\n if (searchService) {\n defaultStrategies = searchService.getDefaultStrategies?.() ?? []\n }\n } catch {\n // Search service may not be available\n }\n\n const fulltextConfigured = Boolean(\n process.env.MEILISEARCH_HOST && process.env.MEILISEARCH_HOST.trim().length > 0\n )\n\n const vectorConfigured = Boolean(\n process.env.OPENAI_API_KEY ||\n process.env.GOOGLE_GENERATIVE_AI_API_KEY ||\n process.env.MISTRAL_API_KEY ||\n process.env.COHERE_API_KEY ||\n process.env.AWS_ACCESS_KEY_ID ||\n process.env.OLLAMA_BASE_URL\n )\n\n const tokensEnabled = process.env.OM_SEARCH_ENABLED !== 'false'\n\n // Check for active reindex locks with heartbeat-based stale detection\n let fulltextReindexLock: ReindexLock | null = null\n let vectorReindexLock: ReindexLock | null = null\n\n if (auth.tenantId) {\n const em = container.resolve('em') as EntityManager\n const db = (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n\n // Check fulltext lock (auto-cleans stale locks based on heartbeat)\n const fulltextLockStatus = await getReindexLockStatus(db, auth.tenantId, { type: 'fulltext' })\n if (fulltextLockStatus) {\n const startedAt = new Date(fulltextLockStatus.startedAt)\n fulltextReindexLock = {\n type: 'fulltext',\n action: fulltextLockStatus.action,\n startedAt: fulltextLockStatus.startedAt,\n elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),\n processedCount: fulltextLockStatus.processedCount,\n totalCount: fulltextLockStatus.totalCount,\n }\n }\n\n // Check vector lock (auto-cleans stale locks based on heartbeat)\n const vectorLockStatus = await getReindexLockStatus(db, auth.tenantId, { type: 'vector' })\n if (vectorLockStatus) {\n const startedAt = new Date(vectorLockStatus.startedAt)\n vectorReindexLock = {\n type: 'vector',\n action: vectorLockStatus.action,\n startedAt: vectorLockStatus.startedAt,\n elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),\n processedCount: vectorLockStatus.processedCount,\n totalCount: vectorLockStatus.totalCount,\n }\n }\n }\n\n // Keep deprecated reindexLock for backwards compatibility (prefer fulltext if both are active)\n const reindexLock = fulltextReindexLock ?? vectorReindexLock\n\n return toJson({\n settings: {\n strategies,\n fulltextConfigured,\n fulltextStats,\n vectorConfigured,\n tokensEnabled,\n defaultStrategies,\n reindexLock,\n fulltextReindexLock,\n vectorReindexLock,\n },\n })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = settingsOpenApi\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { sql } from "kysely";
1
2
  import {
2
3
  prepareJob,
3
4
  finalizeJob
@@ -17,17 +18,17 @@ function buildScope(type, tenantId, organizationId) {
17
18
  partitionCount: null
18
19
  };
19
20
  }
20
- async function getReindexLockStatus(knex, tenantId, options) {
21
+ async function getReindexLockStatus(db, tenantId, options) {
21
22
  const typesToCheck = options?.type ? [options.type] : ["fulltext", "vector"];
22
23
  for (const lockType of typesToCheck) {
23
24
  const entityType = LOCK_ENTITY_TYPES[lockType];
24
25
  try {
25
- const job = await knex("entity_index_jobs").where("entity_type", entityType).whereRaw("tenant_id is not distinct from ?", [tenantId]).whereNull("finished_at").first();
26
+ const job = await db.selectFrom("entity_index_jobs").selectAll().where("entity_type", "=", entityType).where(sql`tenant_id is not distinct from ${tenantId}`).where("finished_at", "is", null).executeTakeFirst();
26
27
  if (!job) continue;
27
28
  const heartbeatAt = job.heartbeat_at ? new Date(job.heartbeat_at).getTime() : 0;
28
29
  const elapsed = Date.now() - heartbeatAt;
29
30
  if (elapsed > HEARTBEAT_STALE_MS) {
30
- await knex("entity_index_jobs").where("id", job.id).update({ finished_at: knex.fn.now() });
31
+ await db.updateTable("entity_index_jobs").set({ finished_at: sql`now()` }).where("id", "=", job.id).execute();
31
32
  continue;
32
33
  }
33
34
  const startedAtStr = job.started_at ? typeof job.started_at === "string" ? job.started_at : new Date(job.started_at).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
@@ -36,9 +37,9 @@ async function getReindexLockStatus(knex, tenantId, options) {
36
37
  action: job.status || "reindexing",
37
38
  startedAt: startedAtStr,
38
39
  tenantId,
39
- organizationId: job.organization_id,
40
- processedCount: job.processed_count,
41
- totalCount: job.total_count
40
+ organizationId: job.organization_id ?? null,
41
+ processedCount: job.processed_count ?? null,
42
+ totalCount: job.total_count ?? null
42
43
  };
43
44
  return result;
44
45
  } catch {
@@ -47,8 +48,8 @@ async function getReindexLockStatus(knex, tenantId, options) {
47
48
  }
48
49
  return null;
49
50
  }
50
- async function acquireReindexLock(knex, options) {
51
- const existing = await getReindexLockStatus(knex, options.tenantId, {
51
+ async function acquireReindexLock(db, options) {
52
+ const existing = await getReindexLockStatus(db, options.tenantId, {
52
53
  type: options.type
53
54
  });
54
55
  if (existing) {
@@ -60,7 +61,7 @@ async function acquireReindexLock(knex, options) {
60
61
  options.tenantId,
61
62
  options.organizationId
62
63
  );
63
- const jobId = await prepareJob(knex, scope, "reindexing", {
64
+ const jobId = await prepareJob(db, scope, "reindexing", {
64
65
  totalCount: options.totalCount
65
66
  });
66
67
  return { acquired: true, jobId: jobId ?? void 0 };
@@ -68,23 +69,25 @@ async function acquireReindexLock(knex, options) {
68
69
  return { acquired: false };
69
70
  }
70
71
  }
71
- async function clearReindexLock(knex, tenantId, type, organizationId) {
72
+ async function clearReindexLock(db, tenantId, type, organizationId) {
72
73
  try {
73
74
  const scope = buildScope(type, tenantId, organizationId);
74
- await finalizeJob(knex, scope);
75
+ await finalizeJob(db, scope);
75
76
  } catch {
76
77
  }
77
78
  }
78
- async function updateReindexProgress(knex, tenantId, type, processedDelta, organizationId) {
79
+ async function updateReindexProgress(db, tenantId, type, processedDelta, organizationId) {
79
80
  try {
80
81
  const scope = buildScope(type, tenantId, organizationId);
81
82
  const entityType = LOCK_ENTITY_TYPES[type];
82
- const updated = await knex("entity_index_jobs").where("entity_type", entityType).whereRaw("tenant_id is not distinct from ?", [tenantId]).whereRaw("organization_id is not distinct from ?", [organizationId ?? null]).whereNull("finished_at").update({
83
- processed_count: knex.raw("coalesce(processed_count, 0) + ?", [Math.max(0, processedDelta)]),
84
- heartbeat_at: knex.fn.now()
85
- });
83
+ const delta = Math.max(0, processedDelta);
84
+ const result = await db.updateTable("entity_index_jobs").set({
85
+ processed_count: sql`coalesce(processed_count, 0) + ${delta}`,
86
+ heartbeat_at: sql`now()`
87
+ }).where("entity_type", "=", entityType).where(sql`tenant_id is not distinct from ${tenantId}`).where(sql`organization_id is not distinct from ${organizationId ?? null}`).where("finished_at", "is", null).executeTakeFirst();
88
+ const updated = Number(result?.numUpdatedRows ?? 0n);
86
89
  if (updated === 0) {
87
- await prepareJob(knex, scope, "reindexing");
90
+ await prepareJob(db, scope, "reindexing");
88
91
  }
89
92
  } catch {
90
93
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/search/lib/reindex-lock.ts"],
4
- "sourcesContent": ["import type { Knex } from 'knex'\nimport {\n prepareJob,\n updateJobProgress,\n finalizeJob,\n type JobScope,\n} from '@open-mercato/core/modules/query_index/lib/jobs'\n\nexport const REINDEX_LOCK_KEY = 'reindex_lock'\n\nexport type ReindexLockType = 'fulltext' | 'vector'\n\n// Entity type mapping for search reindex jobs\nconst LOCK_ENTITY_TYPES: Record<ReindexLockType, string> = {\n fulltext: 'search:reindex:fulltext',\n vector: 'search:reindex:vector',\n}\n\n// Heartbeat staleness threshold (30 seconds)\nconst HEARTBEAT_STALE_MS = 30 * 1000\n\nexport type ReindexLockStatus = {\n type: ReindexLockType\n action: string\n startedAt: string\n tenantId: string\n organizationId?: string | null\n processedCount?: number | null\n totalCount?: number | null\n}\n\nfunction buildScope(\n type: ReindexLockType,\n tenantId: string,\n organizationId?: string | null,\n): JobScope {\n return {\n entityType: LOCK_ENTITY_TYPES[type],\n tenantId,\n organizationId: organizationId ?? null,\n partitionIndex: null,\n partitionCount: null,\n }\n}\n\n/**\n * Check if a reindex operation is currently in progress for a specific type.\n * Returns the lock status if active, null if no lock or lock is stale.\n *\n * Automatically cleans up stale locks (heartbeat older than 60 seconds).\n */\nexport async function getReindexLockStatus(\n knex: Knex,\n tenantId: string,\n options?: { type?: ReindexLockType },\n): Promise<ReindexLockStatus | null> {\n const typesToCheck: ReindexLockType[] = options?.type\n ? [options.type]\n : ['fulltext', 'vector']\n\n for (const lockType of typesToCheck) {\n const entityType = LOCK_ENTITY_TYPES[lockType]\n\n try {\n const job = await knex('entity_index_jobs')\n .where('entity_type', entityType)\n .whereRaw('tenant_id is not distinct from ?', [tenantId])\n .whereNull('finished_at')\n .first()\n\n if (!job) continue\n\n // Check heartbeat staleness\n const heartbeatAt = job.heartbeat_at\n ? new Date(job.heartbeat_at).getTime()\n : 0\n const elapsed = Date.now() - heartbeatAt\n\n if (elapsed > HEARTBEAT_STALE_MS) {\n // Auto-cleanup stale lock\n await knex('entity_index_jobs')\n .where('id', job.id)\n .update({ finished_at: knex.fn.now() })\n continue\n }\n\n // started_at comes as string from knex, convert if needed\n const startedAtStr = job.started_at\n ? (typeof job.started_at === 'string' ? job.started_at : new Date(job.started_at).toISOString())\n : new Date().toISOString()\n\n const result = {\n type: lockType,\n action: job.status || 'reindexing',\n startedAt: startedAtStr,\n tenantId,\n organizationId: job.organization_id,\n processedCount: job.processed_count,\n totalCount: job.total_count,\n }\n return result\n } catch {\n continue\n }\n }\n\n return null\n}\n\n/**\n * Acquire a reindex lock for a specific type. Returns whether lock was acquired.\n * Fulltext and vector locks are independent - they don't block each other.\n */\nexport async function acquireReindexLock(\n knex: Knex,\n options: {\n type: ReindexLockType\n action: string\n tenantId: string\n organizationId?: string | null\n totalCount?: number | null\n },\n): Promise<{ acquired: boolean; jobId?: string }> {\n // Check existing active lock\n const existing = await getReindexLockStatus(knex, options.tenantId, {\n type: options.type,\n })\n if (existing) {\n return { acquired: false }\n }\n\n try {\n const scope = buildScope(\n options.type,\n options.tenantId,\n options.organizationId,\n )\n const jobId = await prepareJob(knex, scope, 'reindexing', {\n totalCount: options.totalCount,\n })\n\n return { acquired: true, jobId: jobId ?? undefined }\n } catch {\n return { acquired: false }\n }\n}\n\n/**\n * Release the reindex lock for a specific type.\n */\nexport async function clearReindexLock(\n knex: Knex,\n tenantId: string,\n type: ReindexLockType,\n organizationId?: string | null,\n): Promise<void> {\n try {\n const scope = buildScope(type, tenantId, organizationId)\n await finalizeJob(knex, scope)\n } catch {\n // Ignore errors when clearing lock\n }\n}\n\n/**\n * Update the reindex progress and refresh the heartbeat.\n * Call this periodically during batch processing to prevent stale lock detection.\n *\n * If no active lock exists (e.g., it expired after queue restart), this will\n * recreate the lock so the reindex button stays disabled while processing.\n */\nexport async function updateReindexProgress(\n knex: Knex,\n tenantId: string,\n type: ReindexLockType,\n processedDelta: number,\n organizationId?: string | null,\n): Promise<void> {\n try {\n const scope = buildScope(type, tenantId, organizationId)\n const entityType = LOCK_ENTITY_TYPES[type]\n\n // Try to update existing active job first\n const updated = await knex('entity_index_jobs')\n .where('entity_type', entityType)\n .whereRaw('tenant_id is not distinct from ?', [tenantId])\n .whereRaw('organization_id is not distinct from ?', [organizationId ?? null])\n .whereNull('finished_at')\n .update({\n processed_count: knex.raw('coalesce(processed_count, 0) + ?', [Math.max(0, processedDelta)]),\n heartbeat_at: knex.fn.now(),\n })\n\n // If no active lock exists, recreate it\n if (updated === 0) {\n await prepareJob(knex, scope, 'reindexing')\n }\n } catch {\n // Ignore errors when updating progress\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\nimport { type Kysely, sql } from 'kysely'\nimport {\n prepareJob,\n finalizeJob,\n type JobScope,\n} from '@open-mercato/core/modules/query_index/lib/jobs'\n\nexport const REINDEX_LOCK_KEY = 'reindex_lock'\n\nexport type ReindexLockType = 'fulltext' | 'vector'\n\n// Entity type mapping for search reindex jobs\nconst LOCK_ENTITY_TYPES: Record<ReindexLockType, string> = {\n fulltext: 'search:reindex:fulltext',\n vector: 'search:reindex:vector',\n}\n\n// Heartbeat staleness threshold (30 seconds)\nconst HEARTBEAT_STALE_MS = 30 * 1000\n\nexport type ReindexLockStatus = {\n type: ReindexLockType\n action: string\n startedAt: string\n tenantId: string\n organizationId?: string | null\n processedCount?: number | null\n totalCount?: number | null\n}\n\nfunction buildScope(\n type: ReindexLockType,\n tenantId: string,\n organizationId?: string | null,\n): JobScope {\n return {\n entityType: LOCK_ENTITY_TYPES[type],\n tenantId,\n organizationId: organizationId ?? null,\n partitionIndex: null,\n partitionCount: null,\n }\n}\n\n/**\n * Check if a reindex operation is currently in progress for a specific type.\n * Returns the lock status if active, null if no lock or lock is stale.\n *\n * Automatically cleans up stale locks (heartbeat older than 60 seconds).\n */\nexport async function getReindexLockStatus(\n db: Kysely<any>,\n tenantId: string,\n options?: { type?: ReindexLockType },\n): Promise<ReindexLockStatus | null> {\n const typesToCheck: ReindexLockType[] = options?.type\n ? [options.type]\n : ['fulltext', 'vector']\n\n for (const lockType of typesToCheck) {\n const entityType = LOCK_ENTITY_TYPES[lockType]\n\n try {\n const job = await db\n .selectFrom('entity_index_jobs' as any)\n .selectAll()\n .where('entity_type' as any, '=', entityType)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)\n .where('finished_at' as any, 'is', null)\n .executeTakeFirst() as {\n id: string\n status?: string | null\n started_at?: Date | string | null\n heartbeat_at?: Date | string | null\n organization_id?: string | null\n processed_count?: number | null\n total_count?: number | null\n } | undefined\n\n if (!job) continue\n\n // Check heartbeat staleness\n const heartbeatAt = job.heartbeat_at\n ? new Date(job.heartbeat_at as string | Date).getTime()\n : 0\n const elapsed = Date.now() - heartbeatAt\n\n if (elapsed > HEARTBEAT_STALE_MS) {\n // Auto-cleanup stale lock\n await db\n .updateTable('entity_index_jobs' as any)\n .set({ finished_at: sql`now()` } as any)\n .where('id' as any, '=', job.id)\n .execute()\n continue\n }\n\n // started_at comes as string from Kysely, convert if needed\n const startedAtStr = job.started_at\n ? (typeof job.started_at === 'string' ? job.started_at : new Date(job.started_at).toISOString())\n : new Date().toISOString()\n\n const result = {\n type: lockType,\n action: job.status || 'reindexing',\n startedAt: startedAtStr,\n tenantId,\n organizationId: job.organization_id ?? null,\n processedCount: job.processed_count ?? null,\n totalCount: job.total_count ?? null,\n }\n return result\n } catch {\n continue\n }\n }\n\n return null\n}\n\n/**\n * Acquire a reindex lock for a specific type. Returns whether lock was acquired.\n * Fulltext and vector locks are independent - they don't block each other.\n */\nexport async function acquireReindexLock(\n db: Kysely<any>,\n options: {\n type: ReindexLockType\n action: string\n tenantId: string\n organizationId?: string | null\n totalCount?: number | null\n },\n): Promise<{ acquired: boolean; jobId?: string }> {\n // Check existing active lock\n const existing = await getReindexLockStatus(db, options.tenantId, {\n type: options.type,\n })\n if (existing) {\n return { acquired: false }\n }\n\n try {\n const scope = buildScope(\n options.type,\n options.tenantId,\n options.organizationId,\n )\n const jobId = await prepareJob(db, scope, 'reindexing', {\n totalCount: options.totalCount,\n })\n\n return { acquired: true, jobId: jobId ?? undefined }\n } catch {\n return { acquired: false }\n }\n}\n\n/**\n * Release the reindex lock for a specific type.\n */\nexport async function clearReindexLock(\n db: Kysely<any>,\n tenantId: string,\n type: ReindexLockType,\n organizationId?: string | null,\n): Promise<void> {\n try {\n const scope = buildScope(type, tenantId, organizationId)\n await finalizeJob(db, scope)\n } catch {\n // Ignore errors when clearing lock\n }\n}\n\n/**\n * Update the reindex progress and refresh the heartbeat.\n * Call this periodically during batch processing to prevent stale lock detection.\n *\n * If no active lock exists (e.g., it expired after queue restart), this will\n * recreate the lock so the reindex button stays disabled while processing.\n */\nexport async function updateReindexProgress(\n db: Kysely<any>,\n tenantId: string,\n type: ReindexLockType,\n processedDelta: number,\n organizationId?: string | null,\n): Promise<void> {\n try {\n const scope = buildScope(type, tenantId, organizationId)\n const entityType = LOCK_ENTITY_TYPES[type]\n const delta = Math.max(0, processedDelta)\n\n // Try to update existing active job first\n const result = await db\n .updateTable('entity_index_jobs' as any)\n .set({\n processed_count: sql`coalesce(processed_count, 0) + ${delta}`,\n heartbeat_at: sql`now()`,\n } as any)\n .where('entity_type' as any, '=', entityType)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)\n .where(sql<boolean>`organization_id is not distinct from ${organizationId ?? null}`)\n .where('finished_at' as any, 'is', null)\n .executeTakeFirst()\n\n // Kysely returns numUpdatedRows as bigint; coerce\n const updated = Number(result?.numUpdatedRows ?? 0n)\n\n // If no active lock exists, recreate it\n if (updated === 0) {\n await prepareJob(db, scope, 'reindexing')\n }\n } catch {\n // Ignore errors when updating progress\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -18,9 +18,9 @@ async function handle(payload, ctx) {
18
18
  }
19
19
  if ((organizationId == null || tenantId == null) && em) {
20
20
  try {
21
- const knex = em.getConnection().getKnex();
21
+ const db = em.getKysely();
22
22
  const table = resolveEntityTableName(em, entityType);
23
- const row = await knex(table).select(["organization_id", "tenant_id"]).where({ id: recordId }).first();
23
+ const row = await db.selectFrom(table).select(["organization_id", "tenant_id"]).where("id", "=", recordId).executeTakeFirst();
24
24
  if (organizationId == null) organizationId = row?.organization_id ?? organizationId;
25
25
  if (tenantId == null) tenantId = row?.tenant_id ?? tenantId;
26
26
  } catch {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/search/subscribers/fulltext_upsert.ts"],
4
- "sourcesContent": ["import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport type { Queue } from '@open-mercato/queue'\nimport type { FulltextIndexJobPayload } from '../../../queue/fulltext-indexing'\nimport { resolveAutoIndexingEnabled } from '../lib/auto-indexing'\nimport { searchDebugWarn, searchError } from '../../../lib/debug'\n\nexport const metadata = { event: 'search.index_record', persistent: false }\n\ntype Payload = {\n entityId?: string\n recordId?: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\nexport default async function handle(payload: Payload, ctx: HandlerContext) {\n const entityType = String(payload?.entityId ?? '')\n const recordId = String(payload?.recordId ?? '')\n\n if (!entityType || !recordId) {\n return\n }\n\n let organizationId = payload?.organizationId ?? null\n let tenantId = payload?.tenantId ?? null\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let em: any | null = null\n try {\n em = ctx.resolve('em')\n } catch {\n em = null\n }\n\n // Resolve missing scope from DB if needed (same pattern as vector_upsert.ts)\n if ((organizationId == null || tenantId == null) && em) {\n try {\n const knex = em.getConnection().getKnex()\n const table = resolveEntityTableName(em, entityType)\n const row = await knex(table).select(['organization_id', 'tenant_id']).where({ id: recordId }).first()\n if (organizationId == null) organizationId = row?.organization_id ?? organizationId\n if (tenantId == null) tenantId = row?.tenant_id ?? tenantId\n } catch {\n // Ignore lookup errors\n }\n }\n\n if (!tenantId) {\n return\n }\n\n const autoIndexingEnabled = await resolveAutoIndexingEnabled(ctx, { defaultValue: true })\n if (!autoIndexingEnabled) {\n return\n }\n\n let queue: Queue<FulltextIndexJobPayload>\n try {\n queue = ctx.resolve<Queue<FulltextIndexJobPayload>>('fulltextIndexQueue')\n } catch {\n searchDebugWarn('search.fulltext', 'fulltextIndexQueue not available, skipping fulltext indexing')\n return\n }\n\n try {\n await queue.enqueue({\n jobType: 'index',\n entityType,\n recordId,\n tenantId: String(tenantId),\n organizationId: organizationId ? String(organizationId) : null,\n })\n } catch (error) {\n searchError('search.fulltext', 'Failed to enqueue fulltext index job', {\n entityType,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n throw error\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,8BAA8B;AAGvC,SAAS,kCAAkC;AAC3C,SAAS,iBAAiB,mBAAmB;AAEtC,MAAM,WAAW,EAAE,OAAO,uBAAuB,YAAY,MAAM;AAW1E,eAAO,OAA8B,SAAkB,KAAqB;AAC1E,QAAM,aAAa,OAAO,SAAS,YAAY,EAAE;AACjD,QAAM,WAAW,OAAO,SAAS,YAAY,EAAE;AAE/C,MAAI,CAAC,cAAc,CAAC,UAAU;AAC5B;AAAA,EACF;AAEA,MAAI,iBAAiB,SAAS,kBAAkB;AAChD,MAAI,WAAW,SAAS,YAAY;AAGpC,MAAI,KAAiB;AACrB,MAAI;AACF,SAAK,IAAI,QAAQ,IAAI;AAAA,EACvB,QAAQ;AACN,SAAK;AAAA,EACP;AAGA,OAAK,kBAAkB,QAAQ,YAAY,SAAS,IAAI;AACtD,QAAI;AACF,YAAM,OAAO,GAAG,cAAc,EAAE,QAAQ;AACxC,YAAM,QAAQ,uBAAuB,IAAI,UAAU;AACnD,YAAM,MAAM,MAAM,KAAK,KAAK,EAAE,OAAO,CAAC,mBAAmB,WAAW,CAAC,EAAE,MAAM,EAAE,IAAI,SAAS,CAAC,EAAE,MAAM;AACrG,UAAI,kBAAkB,KAAM,kBAAiB,KAAK,mBAAmB;AACrE,UAAI,YAAY,KAAM,YAAW,KAAK,aAAa;AAAA,IACrD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,UAAU;AACb;AAAA,EACF;AAEA,QAAM,sBAAsB,MAAM,2BAA2B,KAAK,EAAE,cAAc,KAAK,CAAC;AACxF,MAAI,CAAC,qBAAqB;AACxB;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,YAAQ,IAAI,QAAwC,oBAAoB;AAAA,EAC1E,QAAQ;AACN,oBAAgB,mBAAmB,8DAA8D;AACjG;AAAA,EACF;AAEA,MAAI;AACF,UAAM,MAAM,QAAQ;AAAA,MAClB,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,UAAU,OAAO,QAAQ;AAAA,MACzB,gBAAgB,iBAAiB,OAAO,cAAc,IAAI;AAAA,IAC5D,CAAC;AAAA,EACH,SAAS,OAAO;AACd,gBAAY,mBAAmB,wCAAwC;AAAA,MACrE;AAAA,MACA;AAAA,MACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD,CAAC;AACD,UAAM;AAAA,EACR;AACF;",
4
+ "sourcesContent": ["import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport type { Queue } from '@open-mercato/queue'\nimport type { FulltextIndexJobPayload } from '../../../queue/fulltext-indexing'\nimport { resolveAutoIndexingEnabled } from '../lib/auto-indexing'\nimport { searchDebugWarn, searchError } from '../../../lib/debug'\n\nexport const metadata = { event: 'search.index_record', persistent: false }\n\ntype Payload = {\n entityId?: string\n recordId?: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\nexport default async function handle(payload: Payload, ctx: HandlerContext) {\n const entityType = String(payload?.entityId ?? '')\n const recordId = String(payload?.recordId ?? '')\n\n if (!entityType || !recordId) {\n return\n }\n\n let organizationId = payload?.organizationId ?? null\n let tenantId = payload?.tenantId ?? null\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let em: any | null = null\n try {\n em = ctx.resolve('em')\n } catch {\n em = null\n }\n\n // Resolve missing scope from DB if needed (same pattern as vector_upsert.ts)\n if ((organizationId == null || tenantId == null) && em) {\n try {\n const db = em.getKysely()\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 if (organizationId == null) organizationId = row?.organization_id ?? organizationId\n if (tenantId == null) tenantId = row?.tenant_id ?? tenantId\n } catch {\n // Ignore lookup errors\n }\n }\n\n if (!tenantId) {\n return\n }\n\n const autoIndexingEnabled = await resolveAutoIndexingEnabled(ctx, { defaultValue: true })\n if (!autoIndexingEnabled) {\n return\n }\n\n let queue: Queue<FulltextIndexJobPayload>\n try {\n queue = ctx.resolve<Queue<FulltextIndexJobPayload>>('fulltextIndexQueue')\n } catch {\n searchDebugWarn('search.fulltext', 'fulltextIndexQueue not available, skipping fulltext indexing')\n return\n }\n\n try {\n await queue.enqueue({\n jobType: 'index',\n entityType,\n recordId,\n tenantId: String(tenantId),\n organizationId: organizationId ? String(organizationId) : null,\n })\n } catch (error) {\n searchError('search.fulltext', 'Failed to enqueue fulltext index job', {\n entityType,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n throw error\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,8BAA8B;AAGvC,SAAS,kCAAkC;AAC3C,SAAS,iBAAiB,mBAAmB;AAEtC,MAAM,WAAW,EAAE,OAAO,uBAAuB,YAAY,MAAM;AAW1E,eAAO,OAA8B,SAAkB,KAAqB;AAC1E,QAAM,aAAa,OAAO,SAAS,YAAY,EAAE;AACjD,QAAM,WAAW,OAAO,SAAS,YAAY,EAAE;AAE/C,MAAI,CAAC,cAAc,CAAC,UAAU;AAC5B;AAAA,EACF;AAEA,MAAI,iBAAiB,SAAS,kBAAkB;AAChD,MAAI,WAAW,SAAS,YAAY;AAGpC,MAAI,KAAiB;AACrB,MAAI;AACF,SAAK,IAAI,QAAQ,IAAI;AAAA,EACvB,QAAQ;AACN,SAAK;AAAA,EACP;AAGA,OAAK,kBAAkB,QAAQ,YAAY,SAAS,IAAI;AACtD,QAAI;AACF,YAAM,KAAK,GAAG,UAAU;AACxB,YAAM,QAAQ,uBAAuB,IAAI,UAAU;AACnD,YAAM,MAAM,MAAM,GACf,WAAW,KAAY,EACvB,OAAO,CAAC,mBAA0B,WAAkB,CAAC,EACrD,MAAM,MAAa,KAAK,QAAQ,EAChC,iBAAiB;AACpB,UAAI,kBAAkB,KAAM,kBAAiB,KAAK,mBAAmB;AACrE,UAAI,YAAY,KAAM,YAAW,KAAK,aAAa;AAAA,IACrD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,UAAU;AACb;AAAA,EACF;AAEA,QAAM,sBAAsB,MAAM,2BAA2B,KAAK,EAAE,cAAc,KAAK,CAAC;AACxF,MAAI,CAAC,qBAAqB;AACxB;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,YAAQ,IAAI,QAAwC,oBAAoB;AAAA,EAC1E,QAAQ;AACN,oBAAgB,mBAAmB,8DAA8D;AACjG;AAAA,EACF;AAEA,MAAI;AACF,UAAM,MAAM,QAAQ;AAAA,MAClB,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,UAAU,OAAO,QAAQ;AAAA,MACzB,gBAAgB,iBAAiB,OAAO,cAAc,IAAI;AAAA,IAC5D,CAAC;AAAA,EACH,SAAS,OAAO;AACd,gBAAY,mBAAmB,wCAAwC;AAAA,MACrE;AAAA,MACA;AAAA,MACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD,CAAC;AACD,UAAM;AAAA,EACR;AACF;",
6
6
  "names": []
7
7
  }
@@ -16,9 +16,9 @@ async function handle(payload, ctx) {
16
16
  }
17
17
  if ((organizationId == null || tenantId == null) && em) {
18
18
  try {
19
- const knex = em.getConnection().getKnex();
19
+ const db = em.getKysely();
20
20
  const table = resolveEntityTableName(em, entityType);
21
- const row = await knex(table).select(["organization_id", "tenant_id"]).where({ id: recordId }).first();
21
+ const row = await db.selectFrom(table).select(["organization_id", "tenant_id"]).where("id", "=", recordId).executeTakeFirst();
22
22
  if (organizationId == null) organizationId = row?.organization_id ?? organizationId;
23
23
  if (tenantId == null) tenantId = row?.tenant_id ?? tenantId;
24
24
  } catch {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/search/subscribers/vector_delete.ts"],
4
- "sourcesContent": ["import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport type { Queue } from '@open-mercato/queue'\nimport type { VectorIndexJobPayload } from '../../../queue/vector-indexing'\nimport { resolveAutoIndexingEnabled } from '../lib/auto-indexing'\nimport { searchDebugWarn, searchError } from '../../../lib/debug'\n\nexport const metadata = { event: 'query_index.delete_one', persistent: false }\n\ntype Payload = {\n entityType?: string\n recordId?: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\nexport default async function handle(payload: Payload, ctx: HandlerContext) {\n const entityType = String(payload?.entityType ?? '')\n const recordId = String(payload?.recordId ?? '')\n if (!entityType || !recordId) return\n\n let organizationId = payload?.organizationId ?? null\n let tenantId = payload?.tenantId ?? null\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let em: any | null = null\n try {\n em = ctx.resolve('em')\n } catch {\n em = null\n }\n\n if ((organizationId == null || tenantId == null) && em) {\n try {\n const knex = em.getConnection().getKnex()\n const table = resolveEntityTableName(em, entityType)\n const row = await knex(table).select(['organization_id', 'tenant_id']).where({ id: recordId }).first()\n if (organizationId == null) organizationId = row?.organization_id ?? organizationId\n if (tenantId == null) tenantId = row?.tenant_id ?? tenantId\n } catch {\n // Ignore lookup errors\n }\n }\n\n if (!tenantId) return\n\n const autoIndexingEnabled = await resolveAutoIndexingEnabled(ctx, { defaultValue: true })\n if (!autoIndexingEnabled) return\n\n let queue: Queue<VectorIndexJobPayload>\n try {\n queue = ctx.resolve<Queue<VectorIndexJobPayload>>('vectorIndexQueue')\n } catch {\n searchDebugWarn('search.vector', 'vectorIndexQueue not available, skipping vector delete')\n return\n }\n\n try {\n await queue.enqueue({\n jobType: 'delete',\n entityType,\n recordId,\n tenantId: String(tenantId),\n organizationId: organizationId ? String(organizationId) : null,\n })\n } catch (error) {\n searchError('search.vector', 'Failed to enqueue vector delete job', {\n entityType,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n throw error // Propagate to caller so failure is visible\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,8BAA8B;AAGvC,SAAS,kCAAkC;AAC3C,SAAS,iBAAiB,mBAAmB;AAEtC,MAAM,WAAW,EAAE,OAAO,0BAA0B,YAAY,MAAM;AAW7E,eAAO,OAA8B,SAAkB,KAAqB;AAC1E,QAAM,aAAa,OAAO,SAAS,cAAc,EAAE;AACnD,QAAM,WAAW,OAAO,SAAS,YAAY,EAAE;AAC/C,MAAI,CAAC,cAAc,CAAC,SAAU;AAE9B,MAAI,iBAAiB,SAAS,kBAAkB;AAChD,MAAI,WAAW,SAAS,YAAY;AAGpC,MAAI,KAAiB;AACrB,MAAI;AACF,SAAK,IAAI,QAAQ,IAAI;AAAA,EACvB,QAAQ;AACN,SAAK;AAAA,EACP;AAEA,OAAK,kBAAkB,QAAQ,YAAY,SAAS,IAAI;AACtD,QAAI;AACF,YAAM,OAAO,GAAG,cAAc,EAAE,QAAQ;AACxC,YAAM,QAAQ,uBAAuB,IAAI,UAAU;AACnD,YAAM,MAAM,MAAM,KAAK,KAAK,EAAE,OAAO,CAAC,mBAAmB,WAAW,CAAC,EAAE,MAAM,EAAE,IAAI,SAAS,CAAC,EAAE,MAAM;AACrG,UAAI,kBAAkB,KAAM,kBAAiB,KAAK,mBAAmB;AACrE,UAAI,YAAY,KAAM,YAAW,KAAK,aAAa;AAAA,IACrD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,SAAU;AAEf,QAAM,sBAAsB,MAAM,2BAA2B,KAAK,EAAE,cAAc,KAAK,CAAC;AACxF,MAAI,CAAC,oBAAqB;AAE1B,MAAI;AACJ,MAAI;AACF,YAAQ,IAAI,QAAsC,kBAAkB;AAAA,EACtE,QAAQ;AACN,oBAAgB,iBAAiB,wDAAwD;AACzF;AAAA,EACF;AAEA,MAAI;AACF,UAAM,MAAM,QAAQ;AAAA,MAClB,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,UAAU,OAAO,QAAQ;AAAA,MACzB,gBAAgB,iBAAiB,OAAO,cAAc,IAAI;AAAA,IAC5D,CAAC;AAAA,EACH,SAAS,OAAO;AACd,gBAAY,iBAAiB,uCAAuC;AAAA,MAClE;AAAA,MACA;AAAA,MACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD,CAAC;AACD,UAAM;AAAA,EACR;AACF;",
4
+ "sourcesContent": ["import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport type { Queue } from '@open-mercato/queue'\nimport type { VectorIndexJobPayload } from '../../../queue/vector-indexing'\nimport { resolveAutoIndexingEnabled } from '../lib/auto-indexing'\nimport { searchDebugWarn, searchError } from '../../../lib/debug'\n\nexport const metadata = { event: 'query_index.delete_one', persistent: false }\n\ntype Payload = {\n entityType?: string\n recordId?: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\nexport default async function handle(payload: Payload, ctx: HandlerContext) {\n const entityType = String(payload?.entityType ?? '')\n const recordId = String(payload?.recordId ?? '')\n if (!entityType || !recordId) return\n\n let organizationId = payload?.organizationId ?? null\n let tenantId = payload?.tenantId ?? null\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let em: any | null = null\n try {\n em = ctx.resolve('em')\n } catch {\n em = null\n }\n\n if ((organizationId == null || tenantId == null) && em) {\n try {\n const db = em.getKysely()\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 if (organizationId == null) organizationId = row?.organization_id ?? organizationId\n if (tenantId == null) tenantId = row?.tenant_id ?? tenantId\n } catch {\n // Ignore lookup errors\n }\n }\n\n if (!tenantId) return\n\n const autoIndexingEnabled = await resolveAutoIndexingEnabled(ctx, { defaultValue: true })\n if (!autoIndexingEnabled) return\n\n let queue: Queue<VectorIndexJobPayload>\n try {\n queue = ctx.resolve<Queue<VectorIndexJobPayload>>('vectorIndexQueue')\n } catch {\n searchDebugWarn('search.vector', 'vectorIndexQueue not available, skipping vector delete')\n return\n }\n\n try {\n await queue.enqueue({\n jobType: 'delete',\n entityType,\n recordId,\n tenantId: String(tenantId),\n organizationId: organizationId ? String(organizationId) : null,\n })\n } catch (error) {\n searchError('search.vector', 'Failed to enqueue vector delete job', {\n entityType,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n throw error // Propagate to caller so failure is visible\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,8BAA8B;AAGvC,SAAS,kCAAkC;AAC3C,SAAS,iBAAiB,mBAAmB;AAEtC,MAAM,WAAW,EAAE,OAAO,0BAA0B,YAAY,MAAM;AAW7E,eAAO,OAA8B,SAAkB,KAAqB;AAC1E,QAAM,aAAa,OAAO,SAAS,cAAc,EAAE;AACnD,QAAM,WAAW,OAAO,SAAS,YAAY,EAAE;AAC/C,MAAI,CAAC,cAAc,CAAC,SAAU;AAE9B,MAAI,iBAAiB,SAAS,kBAAkB;AAChD,MAAI,WAAW,SAAS,YAAY;AAGpC,MAAI,KAAiB;AACrB,MAAI;AACF,SAAK,IAAI,QAAQ,IAAI;AAAA,EACvB,QAAQ;AACN,SAAK;AAAA,EACP;AAEA,OAAK,kBAAkB,QAAQ,YAAY,SAAS,IAAI;AACtD,QAAI;AACF,YAAM,KAAK,GAAG,UAAU;AACxB,YAAM,QAAQ,uBAAuB,IAAI,UAAU;AACnD,YAAM,MAAM,MAAM,GACf,WAAW,KAAY,EACvB,OAAO,CAAC,mBAA0B,WAAkB,CAAC,EACrD,MAAM,MAAa,KAAK,QAAQ,EAChC,iBAAiB;AACpB,UAAI,kBAAkB,KAAM,kBAAiB,KAAK,mBAAmB;AACrE,UAAI,YAAY,KAAM,YAAW,KAAK,aAAa;AAAA,IACrD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,SAAU;AAEf,QAAM,sBAAsB,MAAM,2BAA2B,KAAK,EAAE,cAAc,KAAK,CAAC;AACxF,MAAI,CAAC,oBAAqB;AAE1B,MAAI;AACJ,MAAI;AACF,YAAQ,IAAI,QAAsC,kBAAkB;AAAA,EACtE,QAAQ;AACN,oBAAgB,iBAAiB,wDAAwD;AACzF;AAAA,EACF;AAEA,MAAI;AACF,UAAM,MAAM,QAAQ;AAAA,MAClB,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,UAAU,OAAO,QAAQ;AAAA,MACzB,gBAAgB,iBAAiB,OAAO,cAAc,IAAI;AAAA,IAC5D,CAAC;AAAA,EACH,SAAS,OAAO;AACd,gBAAY,iBAAiB,uCAAuC;AAAA,MAClE;AAAA,MACA;AAAA,MACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD,CAAC;AACD,UAAM;AAAA,EACR;AACF;",
6
6
  "names": []
7
7
  }
@@ -16,9 +16,9 @@ async function handle(payload, ctx) {
16
16
  }
17
17
  if ((organizationId == null || tenantId == null) && em) {
18
18
  try {
19
- const knex = em.getConnection().getKnex();
19
+ const db = em.getKysely();
20
20
  const table = resolveEntityTableName(em, entityType);
21
- const row = await knex(table).select(["organization_id", "tenant_id"]).where({ id: recordId }).first();
21
+ const row = await db.selectFrom(table).select(["organization_id", "tenant_id"]).where("id", "=", recordId).executeTakeFirst();
22
22
  if (organizationId == null) organizationId = row?.organization_id ?? organizationId;
23
23
  if (tenantId == null) tenantId = row?.tenant_id ?? tenantId;
24
24
  } catch {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/search/subscribers/vector_upsert.ts"],
4
- "sourcesContent": ["import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport type { Queue } from '@open-mercato/queue'\nimport type { VectorIndexJobPayload } from '../../../queue/vector-indexing'\nimport { resolveAutoIndexingEnabled } from '../lib/auto-indexing'\nimport { searchDebugWarn, searchError } from '../../../lib/debug'\n\nexport const metadata = { event: 'query_index.vectorize_one', persistent: false }\n\ntype Payload = {\n entityType?: string\n recordId?: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\nexport default async function handle(payload: Payload, ctx: HandlerContext) {\n const entityType = String(payload?.entityType ?? '')\n const recordId = String(payload?.recordId ?? '')\n if (!entityType || !recordId) return\n\n let organizationId = payload?.organizationId ?? null\n let tenantId = payload?.tenantId ?? null\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let em: any | null = null\n try {\n em = ctx.resolve('em')\n } catch {\n em = null\n }\n\n if ((organizationId == null || tenantId == null) && em) {\n try {\n const knex = em.getConnection().getKnex()\n const table = resolveEntityTableName(em, entityType)\n const row = await knex(table).select(['organization_id', 'tenant_id']).where({ id: recordId }).first()\n if (organizationId == null) organizationId = row?.organization_id ?? organizationId\n if (tenantId == null) tenantId = row?.tenant_id ?? tenantId\n } catch {\n // Ignore lookup errors\n }\n }\n\n if (!tenantId) return\n\n const autoIndexingEnabled = await resolveAutoIndexingEnabled(ctx, { defaultValue: true })\n if (!autoIndexingEnabled) return\n\n let queue: Queue<VectorIndexJobPayload>\n try {\n queue = ctx.resolve<Queue<VectorIndexJobPayload>>('vectorIndexQueue')\n } catch {\n searchDebugWarn('search.vector', 'vectorIndexQueue not available, skipping vector indexing')\n return\n }\n\n try {\n await queue.enqueue({\n jobType: 'index',\n entityType,\n recordId,\n tenantId: String(tenantId),\n organizationId: organizationId ? String(organizationId) : null,\n })\n } catch (error) {\n searchError('search.vector', 'Failed to enqueue vector index job', {\n entityType,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n throw error // Propagate to caller so failure is visible\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,8BAA8B;AAGvC,SAAS,kCAAkC;AAC3C,SAAS,iBAAiB,mBAAmB;AAEtC,MAAM,WAAW,EAAE,OAAO,6BAA6B,YAAY,MAAM;AAWhF,eAAO,OAA8B,SAAkB,KAAqB;AAC1E,QAAM,aAAa,OAAO,SAAS,cAAc,EAAE;AACnD,QAAM,WAAW,OAAO,SAAS,YAAY,EAAE;AAC/C,MAAI,CAAC,cAAc,CAAC,SAAU;AAE9B,MAAI,iBAAiB,SAAS,kBAAkB;AAChD,MAAI,WAAW,SAAS,YAAY;AAGpC,MAAI,KAAiB;AACrB,MAAI;AACF,SAAK,IAAI,QAAQ,IAAI;AAAA,EACvB,QAAQ;AACN,SAAK;AAAA,EACP;AAEA,OAAK,kBAAkB,QAAQ,YAAY,SAAS,IAAI;AACtD,QAAI;AACF,YAAM,OAAO,GAAG,cAAc,EAAE,QAAQ;AACxC,YAAM,QAAQ,uBAAuB,IAAI,UAAU;AACnD,YAAM,MAAM,MAAM,KAAK,KAAK,EAAE,OAAO,CAAC,mBAAmB,WAAW,CAAC,EAAE,MAAM,EAAE,IAAI,SAAS,CAAC,EAAE,MAAM;AACrG,UAAI,kBAAkB,KAAM,kBAAiB,KAAK,mBAAmB;AACrE,UAAI,YAAY,KAAM,YAAW,KAAK,aAAa;AAAA,IACrD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,SAAU;AAEf,QAAM,sBAAsB,MAAM,2BAA2B,KAAK,EAAE,cAAc,KAAK,CAAC;AACxF,MAAI,CAAC,oBAAqB;AAE1B,MAAI;AACJ,MAAI;AACF,YAAQ,IAAI,QAAsC,kBAAkB;AAAA,EACtE,QAAQ;AACN,oBAAgB,iBAAiB,0DAA0D;AAC3F;AAAA,EACF;AAEA,MAAI;AACF,UAAM,MAAM,QAAQ;AAAA,MAClB,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,UAAU,OAAO,QAAQ;AAAA,MACzB,gBAAgB,iBAAiB,OAAO,cAAc,IAAI;AAAA,IAC5D,CAAC;AAAA,EACH,SAAS,OAAO;AACd,gBAAY,iBAAiB,sCAAsC;AAAA,MACjE;AAAA,MACA;AAAA,MACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD,CAAC;AACD,UAAM;AAAA,EACR;AACF;",
4
+ "sourcesContent": ["import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport type { Queue } from '@open-mercato/queue'\nimport type { VectorIndexJobPayload } from '../../../queue/vector-indexing'\nimport { resolveAutoIndexingEnabled } from '../lib/auto-indexing'\nimport { searchDebugWarn, searchError } from '../../../lib/debug'\n\nexport const metadata = { event: 'query_index.vectorize_one', persistent: false }\n\ntype Payload = {\n entityType?: string\n recordId?: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\nexport default async function handle(payload: Payload, ctx: HandlerContext) {\n const entityType = String(payload?.entityType ?? '')\n const recordId = String(payload?.recordId ?? '')\n if (!entityType || !recordId) return\n\n let organizationId = payload?.organizationId ?? null\n let tenantId = payload?.tenantId ?? null\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let em: any | null = null\n try {\n em = ctx.resolve('em')\n } catch {\n em = null\n }\n\n if ((organizationId == null || tenantId == null) && em) {\n try {\n const db = em.getKysely()\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 if (organizationId == null) organizationId = row?.organization_id ?? organizationId\n if (tenantId == null) tenantId = row?.tenant_id ?? tenantId\n } catch {\n // Ignore lookup errors\n }\n }\n\n if (!tenantId) return\n\n const autoIndexingEnabled = await resolveAutoIndexingEnabled(ctx, { defaultValue: true })\n if (!autoIndexingEnabled) return\n\n let queue: Queue<VectorIndexJobPayload>\n try {\n queue = ctx.resolve<Queue<VectorIndexJobPayload>>('vectorIndexQueue')\n } catch {\n searchDebugWarn('search.vector', 'vectorIndexQueue not available, skipping vector indexing')\n return\n }\n\n try {\n await queue.enqueue({\n jobType: 'index',\n entityType,\n recordId,\n tenantId: String(tenantId),\n organizationId: organizationId ? String(organizationId) : null,\n })\n } catch (error) {\n searchError('search.vector', 'Failed to enqueue vector index job', {\n entityType,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n throw error // Propagate to caller so failure is visible\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,8BAA8B;AAGvC,SAAS,kCAAkC;AAC3C,SAAS,iBAAiB,mBAAmB;AAEtC,MAAM,WAAW,EAAE,OAAO,6BAA6B,YAAY,MAAM;AAWhF,eAAO,OAA8B,SAAkB,KAAqB;AAC1E,QAAM,aAAa,OAAO,SAAS,cAAc,EAAE;AACnD,QAAM,WAAW,OAAO,SAAS,YAAY,EAAE;AAC/C,MAAI,CAAC,cAAc,CAAC,SAAU;AAE9B,MAAI,iBAAiB,SAAS,kBAAkB;AAChD,MAAI,WAAW,SAAS,YAAY;AAGpC,MAAI,KAAiB;AACrB,MAAI;AACF,SAAK,IAAI,QAAQ,IAAI;AAAA,EACvB,QAAQ;AACN,SAAK;AAAA,EACP;AAEA,OAAK,kBAAkB,QAAQ,YAAY,SAAS,IAAI;AACtD,QAAI;AACF,YAAM,KAAK,GAAG,UAAU;AACxB,YAAM,QAAQ,uBAAuB,IAAI,UAAU;AACnD,YAAM,MAAM,MAAM,GACf,WAAW,KAAY,EACvB,OAAO,CAAC,mBAA0B,WAAkB,CAAC,EACrD,MAAM,MAAa,KAAK,QAAQ,EAChC,iBAAiB;AACpB,UAAI,kBAAkB,KAAM,kBAAiB,KAAK,mBAAmB;AACrE,UAAI,YAAY,KAAM,YAAW,KAAK,aAAa;AAAA,IACrD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,SAAU;AAEf,QAAM,sBAAsB,MAAM,2BAA2B,KAAK,EAAE,cAAc,KAAK,CAAC;AACxF,MAAI,CAAC,oBAAqB;AAE1B,MAAI;AACJ,MAAI;AACF,YAAQ,IAAI,QAAsC,kBAAkB;AAAA,EACtE,QAAQ;AACN,oBAAgB,iBAAiB,0DAA0D;AAC3F;AAAA,EACF;AAEA,MAAI;AACF,UAAM,MAAM,QAAQ;AAAA,MAClB,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,UAAU,OAAO,QAAQ;AAAA,MACzB,gBAAgB,iBAAiB,OAAO,cAAc,IAAI;AAAA,IAC5D,CAAC;AAAA,EACH,SAAS,OAAO;AACd,gBAAY,iBAAiB,sCAAsC;AAAA,MACjE;AAAA,MACA;AAAA,MACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD,CAAC;AACD,UAAM;AAAA,EACR;AACF;",
6
6
  "names": []
7
7
  }
@@ -20,13 +20,13 @@ async function handleFulltextIndexJob(job, jobCtx, ctx) {
20
20
  return;
21
21
  }
22
22
  let em = null;
23
- let knex = null;
23
+ let db = null;
24
24
  try {
25
25
  em = ctx.resolve("em");
26
- knex = em.getConnection().getKnex();
26
+ db = em.getKysely();
27
27
  } catch {
28
28
  em = null;
29
- knex = null;
29
+ db = null;
30
30
  }
31
31
  let searchIndexer;
32
32
  try {
@@ -132,8 +132,8 @@ async function handleFulltextIndexJob(job, jobCtx, ctx) {
132
132
  });
133
133
  }
134
134
  }
135
- if (knex && records.length > 0) {
136
- await updateReindexProgress(knex, tenantId, "fulltext", successCount, organizationId ?? null);
135
+ if (db && records.length > 0) {
136
+ await updateReindexProgress(db, tenantId, "fulltext", successCount, organizationId ?? null);
137
137
  }
138
138
  if (progressService && em && records.length > 0) {
139
139
  const completed = await incrementReindexProgress({
@@ -144,8 +144,8 @@ async function handleFulltextIndexJob(job, jobCtx, ctx) {
144
144
  organizationId: organizationId ?? null,
145
145
  delta: successCount
146
146
  });
147
- if (completed && knex) {
148
- await clearReindexLock(knex, tenantId, "fulltext", organizationId ?? null);
147
+ if (completed && db) {
148
+ await clearReindexLock(db, tenantId, "fulltext", organizationId ?? null);
149
149
  }
150
150
  }
151
151
  searchDebug("fulltext-index.worker", "Batch indexed to fulltext", {