@open-mercato/search 0.6.8-develop.7100.1.fbf66fca35 → 0.7.0
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.
- package/AGENTS.md +0 -1
- package/dist/indexer/search-indexer.js +0 -66
- package/dist/indexer/search-indexer.js.map +2 -2
- package/dist/lib/presenter-enricher.js +1 -71
- package/dist/lib/presenter-enricher.js.map +2 -2
- package/dist/modules/search/__integration__/TC-SEARCH-006.spec.js +0 -143
- package/dist/modules/search/__integration__/TC-SEARCH-006.spec.js.map +2 -2
- package/dist/modules/search/lib/entity-access.js +43 -1
- package/dist/modules/search/lib/entity-access.js.map +2 -2
- package/dist/modules/search/workers/fulltext-index.worker.js +24 -7
- package/dist/modules/search/workers/fulltext-index.worker.js.map +2 -2
- package/dist/service.js +1 -18
- package/dist/service.js.map +2 -2
- package/dist/strategies/token.strategy.js +2 -8
- package/dist/strategies/token.strategy.js.map +2 -2
- package/package.json +5 -6
- package/src/__tests__/presenter-enricher.test.ts +0 -234
- package/src/__tests__/service.test.ts +0 -24
- package/src/__tests__/workers.test.ts +17 -46
- package/src/indexer/search-indexer.ts +0 -84
- package/src/lib/presenter-enricher.ts +1 -92
- package/src/modules/search/__integration__/TC-SEARCH-006.spec.ts +2 -190
- package/src/modules/search/api/__tests__/global-search.routes.test.ts +0 -107
- package/src/modules/search/lib/entity-access.ts +130 -4
- package/src/modules/search/workers/fulltext-index.worker.ts +29 -13
- package/src/service.ts +2 -37
- package/src/strategies/token.strategy.ts +2 -15
- package/src/__tests__/search-indexer-batch.test.ts +0 -214
- package/src/__tests__/token-strategy-entity-exclusion.test.ts +0 -99
package/AGENTS.md
CHANGED
|
@@ -557,7 +557,6 @@ curl "https://your-app.com/api/search?q=john%20doe&limit=20" \
|
|
|
557
557
|
| `OM_SEARCH_MAX_TOKENS_PER_FIELD` | When bounding token fan-out for one field | Default: `5000`. Maximum distinct token rows across all values of one field; `0` disables the limit. **Token strategy only** |
|
|
558
558
|
| `OM_SEARCH_MAX_TOKENS_PER_RECORD` | When bounding token fan-out for one record | Default: `20000`. Maximum token rows across all fields in one indexed record; `0` disables the limit. **Token strategy only** |
|
|
559
559
|
| `OM_SEARCH_TOKEN_PRESENCE_CACHE_MS` | When tuning the query engines' "does this entity have search tokens?" probe | Default: `30000` (ms); `0` disables. Process-level TTL cache in `@open-mercato/shared/lib/search/availability` — bounds how long a token purge or first-index is invisible to like/ilike routing. **Token strategy only** |
|
|
560
|
-
| `OM_SEARCH_CUSTOMERS_INDEX_BASE_ENTITY` | When base `customers:customer_entity` rows should stay searchable | Default: `false`. A customer is found through their person/company profile, which carries the detail-page link; the base row adds only a navigation-less duplicate, and one that superadmins saw while nobody else did (#5046). `false` filters the type out of token results; `true` restores the previous behavior, where both rows are returned and the presenter enricher merges the pair. **Read-side only** — the `search_tokens` rows are still written, because `findEntityIdsBySearchTokens` (People/Companies list search) and the query engines' encrypted `like`/`ilike` rewrite resolve ids through them. No reindex needed either way. Policy lives in `packages/core/src/modules/query_index/lib/search-entity-policy.ts`. **Token strategy only** |
|
|
561
560
|
| `SEARCH_EXCLUDE_ENCRYPTED_FIELDS` | When you need to keep encrypted fields out of fulltext | Set to `true` to exclude encrypted fields from fulltext index |
|
|
562
561
|
| `OM_LOG_LEVEL` | When debugging presenter enrichment | Set to `debug` to surface presenter enricher diagnostics (replaces the former `DEBUG_SEARCH_ENRICHER` flag) |
|
|
563
562
|
|
|
@@ -183,72 +183,6 @@ class SearchIndexer {
|
|
|
183
183
|
throw error;
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
-
/**
|
|
187
|
-
* Index a batch of records by id in a single bulk write.
|
|
188
|
-
* Unlike calling indexRecordById() in a loop, this loads each record fresh
|
|
189
|
-
* (same as indexRecordById) but flushes the whole batch through a single
|
|
190
|
-
* searchService.bulkIndex() call. Strategies that implement bulkIndex then
|
|
191
|
-
* collapse the batch into one write; strategies without it still write per
|
|
192
|
-
* record, at the bounded concurrency SearchService applies.
|
|
193
|
-
*/
|
|
194
|
-
async indexRecordsById(params) {
|
|
195
|
-
const { items, tenantId, organizationId } = params;
|
|
196
|
-
if (!this.queryEngine || items.length === 0) {
|
|
197
|
-
return { indexed: 0, skipped: items.length };
|
|
198
|
-
}
|
|
199
|
-
const recordIdsByEntity = /* @__PURE__ */ new Map();
|
|
200
|
-
for (const item of items) {
|
|
201
|
-
const list = recordIdsByEntity.get(item.entityId) ?? [];
|
|
202
|
-
list.push(item.recordId);
|
|
203
|
-
recordIdsByEntity.set(item.entityId, list);
|
|
204
|
-
}
|
|
205
|
-
const allRecords = [];
|
|
206
|
-
let skipped = 0;
|
|
207
|
-
for (const [entityId, recordIds] of recordIdsByEntity) {
|
|
208
|
-
const config = this.entityConfigMap.get(entityId);
|
|
209
|
-
if (!config || config.enabled === false) {
|
|
210
|
-
skipped += recordIds.length;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
for (const recordId of recordIds) {
|
|
214
|
-
try {
|
|
215
|
-
const result = await this.queryEngine.query(entityId, {
|
|
216
|
-
tenantId,
|
|
217
|
-
organizationId: organizationId ?? void 0,
|
|
218
|
-
filters: { id: recordId },
|
|
219
|
-
includeCustomFields: true,
|
|
220
|
-
page: { page: 1, pageSize: 1 },
|
|
221
|
-
skipAutoReindex: true
|
|
222
|
-
});
|
|
223
|
-
const record = result.items[0];
|
|
224
|
-
if (!record) {
|
|
225
|
-
skipped++;
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
const { records: built, dropped } = await this.buildIndexableRecords(
|
|
229
|
-
entityId,
|
|
230
|
-
tenantId,
|
|
231
|
-
organizationId ?? null,
|
|
232
|
-
[record],
|
|
233
|
-
config
|
|
234
|
-
);
|
|
235
|
-
skipped += dropped;
|
|
236
|
-
allRecords.push(...built);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
skipped++;
|
|
239
|
-
searchError("SearchIndexer", "Failed to load record for batch indexing", {
|
|
240
|
-
entityId,
|
|
241
|
-
recordId,
|
|
242
|
-
error: error instanceof Error ? error.message : error
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (allRecords.length > 0) {
|
|
248
|
-
await this.searchService.bulkIndex(allRecords);
|
|
249
|
-
}
|
|
250
|
-
return { indexed: allRecords.length, skipped };
|
|
251
|
-
}
|
|
252
186
|
/**
|
|
253
187
|
* Delete a record from the search index.
|
|
254
188
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/indexer/search-indexer.ts"],
|
|
4
|
-
"sourcesContent": ["import type { SearchService } from '../service'\nimport type {\n SearchModuleConfig,\n SearchEntityConfig,\n SearchBuildContext,\n IndexableRecord,\n SearchResultPresenter,\n SearchResultLink,\n} from '../types'\nimport type { FullTextSearchStrategy } from '../strategies/fulltext.strategy'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { Queue } from '@open-mercato/queue'\nimport type { FulltextIndexJobPayload } from '../queue/fulltext-indexing'\nimport type { VectorIndexJobPayload, VectorBatchRecord } from '../queue/vector-indexing'\nimport { searchDebug, searchDebugWarn, searchError } from '../lib/debug'\n\n/**\n * Maximum number of pages to process during reindex to prevent infinite loops.\n * At 50 records per page, this allows up to 500,000 records per entity.\n */\nconst MAX_PAGES = 10000\n\n/**\n * Parameters for indexing a record.\n */\nexport type IndexRecordParams = {\n entityId: EntityId\n recordId: string\n tenantId: string\n organizationId?: string | null\n record: Record<string, unknown>\n customFields?: Record<string, unknown>\n}\n\n/**\n * Parameters for indexing a batch of records by id in a single bulk write.\n */\nexport type IndexRecordsByIdParams = {\n items: Array<{ entityId: EntityId; recordId: string }>\n tenantId: string\n organizationId?: string | null\n}\n\n/**\n * Parameters for deleting a record from the search index.\n */\nexport type DeleteRecordParams = {\n entityId: EntityId\n recordId: string\n tenantId: string\n}\n\n/**\n * Parameters for purging all records of an entity type.\n */\nexport type PurgeEntityParams = {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n}\n\n/**\n * Parameters for reindexing an entity to fulltext search.\n */\nexport type ReindexEntityParams = {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n /** Whether to recreate the index first (default: true) */\n recreateIndex?: boolean\n /** Callback for progress tracking */\n onProgress?: (progress: ReindexProgress) => void\n /** Whether to use queue for batch processing (default: false) */\n useQueue?: boolean\n}\n\n/**\n * Parameters for reindexing all entities to fulltext search.\n */\nexport type ReindexAllParams = {\n tenantId: string\n organizationId?: string | null\n /** Whether to recreate the index first (default: true) */\n recreateIndex?: boolean\n /** Callback for progress tracking */\n onProgress?: (progress: ReindexProgress) => void\n /** Whether to use queue for batch processing (default: false) */\n useQueue?: boolean\n}\n\n/**\n * Progress information during reindex.\n */\nexport type ReindexProgress = {\n entityId: EntityId\n phase: 'starting' | 'fetching' | 'indexing' | 'complete'\n processed: number\n total?: number\n}\n\n/**\n * Result of a reindex operation.\n */\nexport type ReindexResult = {\n success: boolean\n entitiesProcessed: number\n recordsIndexed: number\n /** Number of records dropped due to missing id or other validation failures */\n recordsDropped?: number\n /** Number of jobs enqueued (when useQueue is true) */\n jobsEnqueued?: number\n errors: Array<{ entityId: EntityId; error: string }>\n}\n\n/**\n * Optional dependencies for SearchIndexer.\n */\nexport type SearchIndexerOptions = {\n queryEngine?: QueryEngine\n /** Queue for fulltext batch indexing */\n fulltextQueue?: Queue<FulltextIndexJobPayload>\n /** Queue for vector batch indexing */\n vectorQueue?: Queue<VectorIndexJobPayload>\n}\n\n/**\n * SearchIndexer orchestrates indexing operations by resolving entity configs\n * and building IndexableRecords for the SearchService.\n */\nexport class SearchIndexer {\n private readonly entityConfigMap: Map<EntityId, SearchEntityConfig>\n private readonly queryEngine?: QueryEngine\n private readonly fulltextQueue?: Queue<FulltextIndexJobPayload>\n private readonly vectorQueue?: Queue<VectorIndexJobPayload>\n\n constructor(\n private readonly searchService: SearchService,\n private readonly moduleConfigs: SearchModuleConfig[],\n options?: SearchIndexerOptions,\n ) {\n this.entityConfigMap = new Map()\n this.queryEngine = options?.queryEngine\n this.fulltextQueue = options?.fulltextQueue\n this.vectorQueue = options?.vectorQueue\n for (const moduleConfig of moduleConfigs) {\n for (const entityConfig of moduleConfig.entities) {\n if (entityConfig.enabled !== false) {\n this.entityConfigMap.set(entityConfig.entityId as EntityId, entityConfig)\n }\n }\n }\n }\n\n /**\n * Returns a wrapped QueryEngine that forces skipAutoReindex: true on every query.\n * Used to prevent feedback loops when search indexing callbacks (buildSource, formatResult)\n * call queryEngine.query() to hydrate related entities.\n */\n private get noReindexQueryEngine(): QueryEngine | undefined {\n if (!this.queryEngine) return undefined\n const wrapped: QueryEngine = {\n query: (entity, opts) => this.queryEngine!.query(entity, { ...opts, skipAutoReindex: true }),\n }\n return wrapped\n }\n\n /**\n * Get the entity config for a given entity ID.\n */\n getEntityConfig(entityId: EntityId): SearchEntityConfig | undefined {\n return this.entityConfigMap.get(entityId)\n }\n\n /**\n * Get all configured entity configs.\n */\n getAllEntityConfigs(): SearchEntityConfig[] {\n return Array.from(this.entityConfigMap.values())\n }\n\n /**\n * Check if an entity is configured for search indexing.\n */\n isEntityEnabled(entityId: EntityId): boolean {\n const config = this.entityConfigMap.get(entityId)\n return config?.enabled !== false\n }\n\n /**\n * Index a record in the search service.\n */\n async indexRecord(params: IndexRecordParams): Promise<void> {\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return // Entity not configured for search\n }\n\n const buildContext: SearchBuildContext = {\n record: params.record,\n customFields: params.customFields ?? {},\n organizationId: params.organizationId,\n tenantId: params.tenantId,\n queryEngine: this.noReindexQueryEngine,\n }\n\n // Try buildSource first (provides text, presenter, links, checksumSource)\n let text: string | string[] | undefined\n let presenter: SearchResultPresenter | undefined\n let url: string | undefined\n let links: SearchResultLink[] | undefined\n let checksumSource: unknown | undefined\n\n if (config.buildSource) {\n try {\n const source = await config.buildSource(buildContext)\n if (source) {\n text = source.text\n if (source.presenter) presenter = source.presenter\n if (source.links) links = source.links\n if (source.checksumSource !== undefined) checksumSource = source.checksumSource\n }\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'buildSource failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Fall back to formatResult if no presenter from buildSource\n if (!presenter && config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'formatResult failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Resolve URL if not already set\n if (!url && config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'resolveUrl failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Resolve links if not already set\n if (!links && config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'resolveLinks failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Build IndexableRecord\n const indexableRecord: IndexableRecord = {\n entityId: params.entityId,\n recordId: params.recordId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n fields: params.record,\n presenter,\n url,\n links,\n text,\n checksumSource,\n }\n\n await this.searchService.index(indexableRecord)\n }\n\n /**\n * Index a record by ID (loads the record from database first).\n * Used by workers that only have record identifiers.\n */\n async indexRecordById(params: {\n entityId: EntityId\n recordId: string\n tenantId: string\n organizationId?: string | null\n }): Promise<{ action: 'indexed' | 'skipped'; reason?: string }> {\n if (!this.queryEngine) {\n return { action: 'skipped', reason: 'queryEngine not available' }\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return { action: 'skipped', reason: 'entity not configured' }\n }\n\n // Load record from database\n try {\n const result = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n filters: { id: params.recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n skipAutoReindex: true,\n })\n\n const record = result.items[0] as Record<string, unknown> | undefined\n if (!record) {\n return { action: 'skipped', reason: 'record not found' }\n }\n\n // Extract custom fields\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (key.startsWith('cf:') || key.startsWith('cf_')) {\n customFields[key.slice(3)] = value\n }\n }\n\n await this.indexRecord({\n entityId: params.entityId,\n recordId: params.recordId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n record,\n customFields,\n })\n\n return { action: 'indexed' }\n } catch (error) {\n searchError('SearchIndexer', 'Failed to load record for indexing', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n throw error\n }\n }\n\n /**\n * Index a batch of records by id in a single bulk write.\n * Unlike calling indexRecordById() in a loop, this loads each record fresh\n * (same as indexRecordById) but flushes the whole batch through a single\n * searchService.bulkIndex() call. Strategies that implement bulkIndex then\n * collapse the batch into one write; strategies without it still write per\n * record, at the bounded concurrency SearchService applies.\n */\n async indexRecordsById(params: IndexRecordsByIdParams): Promise<{ indexed: number; skipped: number }> {\n const { items, tenantId, organizationId } = params\n if (!this.queryEngine || items.length === 0) {\n return { indexed: 0, skipped: items.length }\n }\n\n const recordIdsByEntity = new Map<EntityId, string[]>()\n for (const item of items) {\n const list = recordIdsByEntity.get(item.entityId) ?? []\n list.push(item.recordId)\n recordIdsByEntity.set(item.entityId, list)\n }\n\n const allRecords: IndexableRecord[] = []\n let skipped = 0\n\n for (const [entityId, recordIds] of recordIdsByEntity) {\n const config = this.entityConfigMap.get(entityId)\n if (!config || config.enabled === false) {\n skipped += recordIds.length\n continue\n }\n\n for (const recordId of recordIds) {\n try {\n const result = await this.queryEngine.query(entityId, {\n tenantId,\n organizationId: organizationId ?? undefined,\n filters: { id: recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n skipAutoReindex: true,\n })\n\n const record = result.items[0] as Record<string, unknown> | undefined\n if (!record) {\n skipped++\n continue\n }\n\n const { records: built, dropped } = await this.buildIndexableRecords(\n entityId,\n tenantId,\n organizationId ?? null,\n [record],\n config,\n )\n skipped += dropped\n allRecords.push(...built)\n } catch (error) {\n skipped++\n searchError('SearchIndexer', 'Failed to load record for batch indexing', {\n entityId,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n }\n\n if (allRecords.length > 0) {\n await this.searchService.bulkIndex(allRecords)\n }\n\n return { indexed: allRecords.length, skipped }\n }\n\n /**\n * Delete a record from the search index.\n */\n async deleteRecord(params: DeleteRecordParams): Promise<void> {\n await this.searchService.delete(params.entityId, params.recordId, params.tenantId)\n }\n\n /**\n * Purge all records of an entity type from the search index.\n */\n async purgeEntity(params: PurgeEntityParams): Promise<void> {\n await this.searchService.purge(params.entityId, params.tenantId, params.organizationId)\n }\n\n /**\n * Reindex an entity via all configured strategies (including vector).\n * This is the general reindex method that works with all search strategies.\n */\n async reindexEntity(params: {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n purgeFirst?: boolean\n }): Promise<ReindexResult> {\n if (!this.queryEngine) {\n return {\n success: false,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [{ entityId: params.entityId, error: 'Query engine not available' }],\n }\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return {\n success: false,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [{ entityId: params.entityId, error: 'Entity not configured for search' }],\n }\n }\n\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 1,\n recordsIndexed: 0,\n errors: [],\n }\n\n // Optionally purge first\n if (params.purgeFirst) {\n try {\n await this.searchService.purge(params.entityId, params.tenantId, params.organizationId)\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to purge entity before reindex', {\n entityId: params.entityId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Paginate through all records\n let page = 1\n const pageSize = 200\n let hasMore = true\n\n while (hasMore && page <= MAX_PAGES) {\n try {\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n includeCustomFields: true,\n page: { page, pageSize },\n skipAutoReindex: true,\n })\n\n const items = queryResult.items as Record<string, unknown>[]\n if (items.length === 0) {\n hasMore = false\n break\n }\n\n // Build and index records\n const { records } = await this.buildIndexableRecords(\n params.entityId,\n params.tenantId,\n params.organizationId ?? null,\n items,\n config,\n )\n\n // Bulk index records via SearchService (sends to all strategies)\n if (records.length > 0) {\n try {\n await this.searchService.bulkIndex(records)\n result.recordsIndexed += records.length\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to bulk index records', {\n entityId: params.entityId,\n recordCount: records.length,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n page++\n hasMore = items.length === pageSize\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n break\n }\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities via all configured strategies.\n */\n async reindexAll(params: {\n tenantId: string\n organizationId?: string | null\n purgeFirst?: boolean\n }): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [],\n }\n\n const enabledEntities = this.listEnabledEntities()\n\n for (const entityId of enabledEntities) {\n const entityResult = await this.reindexEntity({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n purgeFirst: params.purgeFirst,\n })\n\n result.entitiesProcessed++\n result.recordsIndexed += entityResult.recordsIndexed\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Bulk index multiple records.\n */\n async bulkIndexRecords(params: IndexRecordParams[]): Promise<void> {\n const indexableRecords: IndexableRecord[] = []\n\n for (const param of params) {\n const config = this.entityConfigMap.get(param.entityId)\n if (!config || config.enabled === false) {\n continue\n }\n\n const buildContext: SearchBuildContext = {\n record: param.record,\n customFields: param.customFields ?? {},\n organizationId: param.organizationId,\n tenantId: param.tenantId,\n }\n\n let presenter: SearchResultPresenter | undefined\n if (config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch {\n // Skip presenter on error\n }\n }\n\n let url: string | undefined\n if (config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch {\n // Skip URL on error\n }\n }\n\n let links: SearchResultLink[] | undefined\n if (config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch {\n // Skip links on error\n }\n }\n\n indexableRecords.push({\n entityId: param.entityId,\n recordId: param.recordId,\n tenantId: param.tenantId,\n organizationId: param.organizationId,\n fields: param.record,\n presenter,\n url,\n links,\n })\n }\n\n if (indexableRecords.length > 0) {\n try {\n await this.searchService.bulkIndex(indexableRecords)\n } catch (error) {\n throw new Error(\n `Failed to bulk index ${indexableRecords.length} records: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n }\n }\n\n /**\n * List all enabled entity IDs from the module configurations.\n */\n listEnabledEntities(): EntityId[] {\n return Array.from(this.entityConfigMap.keys())\n }\n\n /**\n * Get the fulltext strategy from the search service.\n */\n private getFulltextStrategy(): FullTextSearchStrategy | undefined {\n const strategy = this.searchService.getStrategy('fulltext')\n if (!strategy) return undefined\n return strategy as FullTextSearchStrategy\n }\n\n /**\n * Reindex a single entity type to fulltext search.\n * This fetches all records from the database and re-indexes them to fulltext only.\n *\n * When `useQueue` is true, batches are enqueued for background processing by workers.\n * When `useQueue` is false (default), batches are indexed directly (blocking).\n */\n async reindexEntityToFulltext(params: ReindexEntityParams): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n const fulltext = this.getFulltextStrategy()\n if (!fulltext) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Fulltext strategy not available' })\n return result\n }\n\n // If useQueue is requested but no queue is available, return error\n if (params.useQueue && !this.fulltextQueue) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Fulltext queue not configured for queue-based reindexing' })\n return result\n }\n\n if (!this.queryEngine) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'QueryEngine not available for reindexing' })\n return result\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Entity not configured for search' })\n return result\n }\n\n try {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'starting',\n processed: 0,\n })\n\n // Recreate index if requested (default: true)\n if (params.recreateIndex !== false) {\n await fulltext.recreateIndex(params.tenantId)\n }\n\n // Fetch and index records with pagination\n const pageSize = 200\n let page = 1\n let totalProcessed = 0\n let jobsEnqueued = 0\n\n for (;;) {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'fetching',\n processed: totalProcessed,\n })\n\n try {\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n page: { page, pageSize },\n skipAutoReindex: true,\n })\n\n if (!queryResult.items.length) {\n break\n }\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'indexing',\n processed: totalProcessed,\n total: queryResult.total,\n })\n\n // Build IndexableRecords for this batch\n const { records: indexableRecords, dropped } = await this.buildIndexableRecords(\n params.entityId,\n params.tenantId,\n params.organizationId ?? null,\n queryResult.items,\n config,\n )\n result.recordsDropped = (result.recordsDropped ?? 0) + dropped\n\n // Index to fulltext - either via queue or directly\n if (indexableRecords.length > 0) {\n if (params.useQueue && this.fulltextQueue) {\n // Enqueue batch for background processing - only pass minimal references\n // Worker will load fresh data from entity_indexes table\n await this.fulltextQueue.enqueue({\n jobType: 'batch-index',\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n records: indexableRecords.map((r) => ({ entityId: r.entityId, recordId: r.recordId })),\n })\n jobsEnqueued += 1\n totalProcessed += indexableRecords.length\n } else {\n // Direct indexing (blocking)\n try {\n await fulltext.bulkIndex(indexableRecords)\n totalProcessed += indexableRecords.length\n } catch (indexError) {\n // Log error but continue with remaining batches\n const errorMsg = indexError instanceof Error ? indexError.message : String(indexError)\n result.errors.push({\n entityId: params.entityId,\n error: `Batch ${page} failed: ${errorMsg}`,\n })\n }\n }\n }\n\n if (queryResult.items.length < pageSize) {\n break\n }\n page += 1\n\n // Safety check to prevent infinite loops\n if (page > MAX_PAGES) {\n break\n }\n } catch (queryError) {\n const errorMsg = queryError instanceof Error ? queryError.message : String(queryError)\n result.errors.push({\n entityId: params.entityId,\n error: `Query failed: ${errorMsg}`,\n })\n break\n }\n }\n\n result.entitiesProcessed = 1\n result.recordsIndexed = totalProcessed\n result.jobsEnqueued = jobsEnqueued\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'complete',\n processed: totalProcessed,\n total: totalProcessed,\n })\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities to fulltext search.\n *\n * When `useQueue` is true, batches are enqueued for background processing by workers.\n * When `useQueue` is false (default), batches are indexed directly (blocking).\n */\n async reindexAllToFulltext(params: ReindexAllParams): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n const fulltext = this.getFulltextStrategy()\n if (!fulltext) {\n result.success = false\n result.errors.push({ entityId: 'all' as EntityId, error: 'Fulltext strategy not available' })\n return result\n }\n\n // Recreate index once before processing all entities\n if (params.recreateIndex !== false) {\n await fulltext.recreateIndex(params.tenantId)\n }\n\n const entities = this.listEnabledEntities()\n\n for (const entityId of entities) {\n const entityResult = await this.reindexEntityToFulltext({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n recreateIndex: false, // Already recreated above\n onProgress: params.onProgress,\n useQueue: params.useQueue,\n })\n\n result.entitiesProcessed += entityResult.entitiesProcessed\n result.recordsIndexed += entityResult.recordsIndexed\n result.recordsDropped = (result.recordsDropped ?? 0) + (entityResult.recordsDropped ?? 0)\n result.jobsEnqueued = (result.jobsEnqueued ?? 0) + (entityResult.jobsEnqueued ?? 0)\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Reindex a single entity type to vector search.\n * This fetches all records from the database and enqueues them for vector indexing.\n *\n * When `useQueue` is true (default), record IDs are enqueued for background processing by workers.\n * When `useQueue` is false, records are indexed directly (blocking).\n */\n async reindexEntityToVector(params: ReindexEntityParams & { purgeFirst?: boolean }): Promise<ReindexResult> {\n searchDebug('SearchIndexer', 'reindexEntityToVector called', {\n entityId: params.entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n useQueue: params.useQueue,\n purgeFirst: params.purgeFirst,\n })\n\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n // If useQueue is requested but no queue is available, return error\n if (params.useQueue !== false && !this.vectorQueue) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Vector queue not configured for queue-based reindexing' })\n return result\n }\n\n if (!this.queryEngine) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'QueryEngine not available for reindexing' })\n return result\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Entity not configured for search' })\n return result\n }\n\n try {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'starting',\n processed: 0,\n })\n\n // Optionally purge vector index first\n if (params.purgeFirst) {\n try {\n await this.searchService.purge(params.entityId, params.tenantId, params.organizationId)\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to purge entity before vector reindex', {\n entityId: params.entityId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Fetch and enqueue records with pagination\n const pageSize = 200\n let page = 1\n let totalProcessed = 0\n let jobsEnqueued = 0\n\n for (;;) {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'fetching',\n processed: totalProcessed,\n })\n\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n page: { page, pageSize },\n skipAutoReindex: true,\n })\n\n if (!queryResult.items.length) break\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'indexing',\n processed: totalProcessed,\n total: queryResult.total,\n })\n\n // Build batch of record references\n const batchRecords: VectorBatchRecord[] = []\n for (const item of queryResult.items) {\n const recordId = String((item as Record<string, unknown>).id ?? '')\n if (!recordId) {\n result.recordsDropped = (result.recordsDropped ?? 0) + 1\n continue\n }\n batchRecords.push({\n entityId: params.entityId,\n recordId,\n })\n }\n\n // Enqueue batch for background processing or index directly\n if (batchRecords.length > 0) {\n if (params.useQueue !== false && this.vectorQueue) {\n await this.vectorQueue.enqueue({\n jobType: 'batch-index',\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n records: batchRecords,\n })\n jobsEnqueued += 1\n totalProcessed += batchRecords.length\n searchDebug('SearchIndexer', 'Enqueued batch for vector indexing', {\n entityId: params.entityId,\n batchSize: batchRecords.length,\n jobsEnqueued,\n totalProcessed,\n })\n } else {\n // Direct indexing (blocking) - index each record via SearchService\n for (const { entityId, recordId } of batchRecords) {\n try {\n await this.indexRecordById({\n entityId: entityId as EntityId,\n recordId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n })\n totalProcessed++\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to index record to vector', {\n entityId,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n }\n }\n\n if (queryResult.items.length < pageSize) break\n page += 1\n\n // Safety check to prevent infinite loops\n if (page > MAX_PAGES) {\n searchDebugWarn('SearchIndexer', 'Reached MAX_PAGES limit, stopping pagination', {\n entityId: params.entityId,\n maxPages: MAX_PAGES,\n totalProcessed,\n })\n break\n }\n }\n\n result.entitiesProcessed = 1\n result.recordsIndexed = totalProcessed\n result.jobsEnqueued = jobsEnqueued\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'complete',\n processed: totalProcessed,\n total: totalProcessed,\n })\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities to vector search.\n *\n * When `useQueue` is true (default), batches are enqueued for background processing by workers.\n * When `useQueue` is false, batches are indexed directly (blocking).\n */\n async reindexAllToVector(params: ReindexAllParams & { purgeFirst?: boolean }): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n const entities = this.listEnabledEntities()\n for (const entityId of entities) {\n const entityResult = await this.reindexEntityToVector({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n onProgress: params.onProgress,\n useQueue: params.useQueue,\n purgeFirst: params.purgeFirst,\n })\n\n result.entitiesProcessed += entityResult.entitiesProcessed\n result.recordsIndexed += entityResult.recordsIndexed\n result.recordsDropped = (result.recordsDropped ?? 0) + (entityResult.recordsDropped ?? 0)\n result.jobsEnqueued = (result.jobsEnqueued ?? 0) + (entityResult.jobsEnqueued ?? 0)\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Build IndexableRecords from raw query results.\n * Returns records and count of dropped items (missing id or other validation failures).\n */\n private async buildIndexableRecords(\n entityId: EntityId,\n tenantId: string,\n organizationId: string | null,\n items: Record<string, unknown>[],\n config: SearchEntityConfig,\n ): Promise<{ records: IndexableRecord[]; dropped: number }> {\n const records: IndexableRecord[] = []\n let dropped = 0\n\n // Debug: log first item to see structure\n if (items.length > 0) {\n searchDebug('SearchIndexer', 'Sample item structure', {\n entityId,\n sampleKeys: Object.keys(items[0]),\n sampleId: items[0].id,\n hasId: 'id' in items[0],\n firstName: items[0].first_name,\n lastName: items[0].last_name,\n preferredName: items[0].preferred_name,\n sampleItem: JSON.stringify(items[0]).slice(0, 500),\n })\n }\n\n for (const item of items) {\n const recordId = String(item.id ?? '')\n if (!recordId) {\n searchDebugWarn('SearchIndexer', 'Skipping item without id', { entityId, itemKeys: Object.keys(item) })\n dropped++\n continue\n }\n\n // Extract custom fields from record\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(item)) {\n if (key.startsWith('cf:') || key.startsWith('cf_')) {\n const cfKey = key.slice(3) // Remove 'cf:' or 'cf_' prefix (both are 3 chars)\n customFields[cfKey] = value\n }\n }\n\n const buildContext: SearchBuildContext = {\n record: item,\n customFields,\n organizationId,\n tenantId,\n queryEngine: this.noReindexQueryEngine,\n }\n\n // Try buildSource first (provides text, presenter, links, checksumSource)\n let text: string | string[] | undefined\n let presenter: SearchResultPresenter | undefined\n let url: string | undefined\n let links: SearchResultLink[] | undefined\n let checksumSource: unknown | undefined\n\n if (config.buildSource) {\n try {\n const source = await config.buildSource(buildContext)\n if (source) {\n text = source.text\n if (source.presenter) presenter = source.presenter\n if (source.links) links = source.links\n if (source.checksumSource !== undefined) checksumSource = source.checksumSource\n }\n } catch (err) {\n searchDebugWarn('SearchIndexer', 'buildSource failed', {\n entityId,\n recordId,\n error: err instanceof Error ? err.message : err,\n })\n }\n }\n\n // Fall back to formatResult if no presenter from buildSource\n if (!presenter && config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch {\n // Skip presenter on error\n }\n }\n\n // Resolve URL if not already set\n if (!url && config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch {\n // Skip URL on error\n }\n }\n\n // Resolve links if not already set\n if (!links && config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch {\n // Skip links on error\n }\n }\n\n records.push({\n entityId,\n recordId,\n tenantId,\n organizationId,\n fields: item,\n presenter,\n url,\n links,\n text,\n checksumSource,\n })\n }\n\n searchDebug('SearchIndexer', 'Finished building records', {\n entityId,\n inputCount: items.length,\n outputCount: records.length,\n dropped,\n })\n\n return { records, dropped }\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { SearchService } from '../service'\nimport type {\n SearchModuleConfig,\n SearchEntityConfig,\n SearchBuildContext,\n IndexableRecord,\n SearchResultPresenter,\n SearchResultLink,\n} from '../types'\nimport type { FullTextSearchStrategy } from '../strategies/fulltext.strategy'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { Queue } from '@open-mercato/queue'\nimport type { FulltextIndexJobPayload } from '../queue/fulltext-indexing'\nimport type { VectorIndexJobPayload, VectorBatchRecord } from '../queue/vector-indexing'\nimport { searchDebug, searchDebugWarn, searchError } from '../lib/debug'\n\n/**\n * Maximum number of pages to process during reindex to prevent infinite loops.\n * At 50 records per page, this allows up to 500,000 records per entity.\n */\nconst MAX_PAGES = 10000\n\n/**\n * Parameters for indexing a record.\n */\nexport type IndexRecordParams = {\n entityId: EntityId\n recordId: string\n tenantId: string\n organizationId?: string | null\n record: Record<string, unknown>\n customFields?: Record<string, unknown>\n}\n\n/**\n * Parameters for deleting a record from the search index.\n */\nexport type DeleteRecordParams = {\n entityId: EntityId\n recordId: string\n tenantId: string\n}\n\n/**\n * Parameters for purging all records of an entity type.\n */\nexport type PurgeEntityParams = {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n}\n\n/**\n * Parameters for reindexing an entity to fulltext search.\n */\nexport type ReindexEntityParams = {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n /** Whether to recreate the index first (default: true) */\n recreateIndex?: boolean\n /** Callback for progress tracking */\n onProgress?: (progress: ReindexProgress) => void\n /** Whether to use queue for batch processing (default: false) */\n useQueue?: boolean\n}\n\n/**\n * Parameters for reindexing all entities to fulltext search.\n */\nexport type ReindexAllParams = {\n tenantId: string\n organizationId?: string | null\n /** Whether to recreate the index first (default: true) */\n recreateIndex?: boolean\n /** Callback for progress tracking */\n onProgress?: (progress: ReindexProgress) => void\n /** Whether to use queue for batch processing (default: false) */\n useQueue?: boolean\n}\n\n/**\n * Progress information during reindex.\n */\nexport type ReindexProgress = {\n entityId: EntityId\n phase: 'starting' | 'fetching' | 'indexing' | 'complete'\n processed: number\n total?: number\n}\n\n/**\n * Result of a reindex operation.\n */\nexport type ReindexResult = {\n success: boolean\n entitiesProcessed: number\n recordsIndexed: number\n /** Number of records dropped due to missing id or other validation failures */\n recordsDropped?: number\n /** Number of jobs enqueued (when useQueue is true) */\n jobsEnqueued?: number\n errors: Array<{ entityId: EntityId; error: string }>\n}\n\n/**\n * Optional dependencies for SearchIndexer.\n */\nexport type SearchIndexerOptions = {\n queryEngine?: QueryEngine\n /** Queue for fulltext batch indexing */\n fulltextQueue?: Queue<FulltextIndexJobPayload>\n /** Queue for vector batch indexing */\n vectorQueue?: Queue<VectorIndexJobPayload>\n}\n\n/**\n * SearchIndexer orchestrates indexing operations by resolving entity configs\n * and building IndexableRecords for the SearchService.\n */\nexport class SearchIndexer {\n private readonly entityConfigMap: Map<EntityId, SearchEntityConfig>\n private readonly queryEngine?: QueryEngine\n private readonly fulltextQueue?: Queue<FulltextIndexJobPayload>\n private readonly vectorQueue?: Queue<VectorIndexJobPayload>\n\n constructor(\n private readonly searchService: SearchService,\n private readonly moduleConfigs: SearchModuleConfig[],\n options?: SearchIndexerOptions,\n ) {\n this.entityConfigMap = new Map()\n this.queryEngine = options?.queryEngine\n this.fulltextQueue = options?.fulltextQueue\n this.vectorQueue = options?.vectorQueue\n for (const moduleConfig of moduleConfigs) {\n for (const entityConfig of moduleConfig.entities) {\n if (entityConfig.enabled !== false) {\n this.entityConfigMap.set(entityConfig.entityId as EntityId, entityConfig)\n }\n }\n }\n }\n\n /**\n * Returns a wrapped QueryEngine that forces skipAutoReindex: true on every query.\n * Used to prevent feedback loops when search indexing callbacks (buildSource, formatResult)\n * call queryEngine.query() to hydrate related entities.\n */\n private get noReindexQueryEngine(): QueryEngine | undefined {\n if (!this.queryEngine) return undefined\n const wrapped: QueryEngine = {\n query: (entity, opts) => this.queryEngine!.query(entity, { ...opts, skipAutoReindex: true }),\n }\n return wrapped\n }\n\n /**\n * Get the entity config for a given entity ID.\n */\n getEntityConfig(entityId: EntityId): SearchEntityConfig | undefined {\n return this.entityConfigMap.get(entityId)\n }\n\n /**\n * Get all configured entity configs.\n */\n getAllEntityConfigs(): SearchEntityConfig[] {\n return Array.from(this.entityConfigMap.values())\n }\n\n /**\n * Check if an entity is configured for search indexing.\n */\n isEntityEnabled(entityId: EntityId): boolean {\n const config = this.entityConfigMap.get(entityId)\n return config?.enabled !== false\n }\n\n /**\n * Index a record in the search service.\n */\n async indexRecord(params: IndexRecordParams): Promise<void> {\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return // Entity not configured for search\n }\n\n const buildContext: SearchBuildContext = {\n record: params.record,\n customFields: params.customFields ?? {},\n organizationId: params.organizationId,\n tenantId: params.tenantId,\n queryEngine: this.noReindexQueryEngine,\n }\n\n // Try buildSource first (provides text, presenter, links, checksumSource)\n let text: string | string[] | undefined\n let presenter: SearchResultPresenter | undefined\n let url: string | undefined\n let links: SearchResultLink[] | undefined\n let checksumSource: unknown | undefined\n\n if (config.buildSource) {\n try {\n const source = await config.buildSource(buildContext)\n if (source) {\n text = source.text\n if (source.presenter) presenter = source.presenter\n if (source.links) links = source.links\n if (source.checksumSource !== undefined) checksumSource = source.checksumSource\n }\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'buildSource failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Fall back to formatResult if no presenter from buildSource\n if (!presenter && config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'formatResult failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Resolve URL if not already set\n if (!url && config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'resolveUrl failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Resolve links if not already set\n if (!links && config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'resolveLinks failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Build IndexableRecord\n const indexableRecord: IndexableRecord = {\n entityId: params.entityId,\n recordId: params.recordId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n fields: params.record,\n presenter,\n url,\n links,\n text,\n checksumSource,\n }\n\n await this.searchService.index(indexableRecord)\n }\n\n /**\n * Index a record by ID (loads the record from database first).\n * Used by workers that only have record identifiers.\n */\n async indexRecordById(params: {\n entityId: EntityId\n recordId: string\n tenantId: string\n organizationId?: string | null\n }): Promise<{ action: 'indexed' | 'skipped'; reason?: string }> {\n if (!this.queryEngine) {\n return { action: 'skipped', reason: 'queryEngine not available' }\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return { action: 'skipped', reason: 'entity not configured' }\n }\n\n // Load record from database\n try {\n const result = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n filters: { id: params.recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n skipAutoReindex: true,\n })\n\n const record = result.items[0] as Record<string, unknown> | undefined\n if (!record) {\n return { action: 'skipped', reason: 'record not found' }\n }\n\n // Extract custom fields\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (key.startsWith('cf:') || key.startsWith('cf_')) {\n customFields[key.slice(3)] = value\n }\n }\n\n await this.indexRecord({\n entityId: params.entityId,\n recordId: params.recordId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n record,\n customFields,\n })\n\n return { action: 'indexed' }\n } catch (error) {\n searchError('SearchIndexer', 'Failed to load record for indexing', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n throw error\n }\n }\n\n /**\n * Delete a record from the search index.\n */\n async deleteRecord(params: DeleteRecordParams): Promise<void> {\n await this.searchService.delete(params.entityId, params.recordId, params.tenantId)\n }\n\n /**\n * Purge all records of an entity type from the search index.\n */\n async purgeEntity(params: PurgeEntityParams): Promise<void> {\n await this.searchService.purge(params.entityId, params.tenantId, params.organizationId)\n }\n\n /**\n * Reindex an entity via all configured strategies (including vector).\n * This is the general reindex method that works with all search strategies.\n */\n async reindexEntity(params: {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n purgeFirst?: boolean\n }): Promise<ReindexResult> {\n if (!this.queryEngine) {\n return {\n success: false,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [{ entityId: params.entityId, error: 'Query engine not available' }],\n }\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return {\n success: false,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [{ entityId: params.entityId, error: 'Entity not configured for search' }],\n }\n }\n\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 1,\n recordsIndexed: 0,\n errors: [],\n }\n\n // Optionally purge first\n if (params.purgeFirst) {\n try {\n await this.searchService.purge(params.entityId, params.tenantId, params.organizationId)\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to purge entity before reindex', {\n entityId: params.entityId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Paginate through all records\n let page = 1\n const pageSize = 200\n let hasMore = true\n\n while (hasMore && page <= MAX_PAGES) {\n try {\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n includeCustomFields: true,\n page: { page, pageSize },\n skipAutoReindex: true,\n })\n\n const items = queryResult.items as Record<string, unknown>[]\n if (items.length === 0) {\n hasMore = false\n break\n }\n\n // Build and index records\n const { records } = await this.buildIndexableRecords(\n params.entityId,\n params.tenantId,\n params.organizationId ?? null,\n items,\n config,\n )\n\n // Bulk index records via SearchService (sends to all strategies)\n if (records.length > 0) {\n try {\n await this.searchService.bulkIndex(records)\n result.recordsIndexed += records.length\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to bulk index records', {\n entityId: params.entityId,\n recordCount: records.length,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n page++\n hasMore = items.length === pageSize\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n break\n }\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities via all configured strategies.\n */\n async reindexAll(params: {\n tenantId: string\n organizationId?: string | null\n purgeFirst?: boolean\n }): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [],\n }\n\n const enabledEntities = this.listEnabledEntities()\n\n for (const entityId of enabledEntities) {\n const entityResult = await this.reindexEntity({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n purgeFirst: params.purgeFirst,\n })\n\n result.entitiesProcessed++\n result.recordsIndexed += entityResult.recordsIndexed\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Bulk index multiple records.\n */\n async bulkIndexRecords(params: IndexRecordParams[]): Promise<void> {\n const indexableRecords: IndexableRecord[] = []\n\n for (const param of params) {\n const config = this.entityConfigMap.get(param.entityId)\n if (!config || config.enabled === false) {\n continue\n }\n\n const buildContext: SearchBuildContext = {\n record: param.record,\n customFields: param.customFields ?? {},\n organizationId: param.organizationId,\n tenantId: param.tenantId,\n }\n\n let presenter: SearchResultPresenter | undefined\n if (config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch {\n // Skip presenter on error\n }\n }\n\n let url: string | undefined\n if (config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch {\n // Skip URL on error\n }\n }\n\n let links: SearchResultLink[] | undefined\n if (config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch {\n // Skip links on error\n }\n }\n\n indexableRecords.push({\n entityId: param.entityId,\n recordId: param.recordId,\n tenantId: param.tenantId,\n organizationId: param.organizationId,\n fields: param.record,\n presenter,\n url,\n links,\n })\n }\n\n if (indexableRecords.length > 0) {\n try {\n await this.searchService.bulkIndex(indexableRecords)\n } catch (error) {\n throw new Error(\n `Failed to bulk index ${indexableRecords.length} records: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n }\n }\n\n /**\n * List all enabled entity IDs from the module configurations.\n */\n listEnabledEntities(): EntityId[] {\n return Array.from(this.entityConfigMap.keys())\n }\n\n /**\n * Get the fulltext strategy from the search service.\n */\n private getFulltextStrategy(): FullTextSearchStrategy | undefined {\n const strategy = this.searchService.getStrategy('fulltext')\n if (!strategy) return undefined\n return strategy as FullTextSearchStrategy\n }\n\n /**\n * Reindex a single entity type to fulltext search.\n * This fetches all records from the database and re-indexes them to fulltext only.\n *\n * When `useQueue` is true, batches are enqueued for background processing by workers.\n * When `useQueue` is false (default), batches are indexed directly (blocking).\n */\n async reindexEntityToFulltext(params: ReindexEntityParams): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n const fulltext = this.getFulltextStrategy()\n if (!fulltext) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Fulltext strategy not available' })\n return result\n }\n\n // If useQueue is requested but no queue is available, return error\n if (params.useQueue && !this.fulltextQueue) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Fulltext queue not configured for queue-based reindexing' })\n return result\n }\n\n if (!this.queryEngine) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'QueryEngine not available for reindexing' })\n return result\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Entity not configured for search' })\n return result\n }\n\n try {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'starting',\n processed: 0,\n })\n\n // Recreate index if requested (default: true)\n if (params.recreateIndex !== false) {\n await fulltext.recreateIndex(params.tenantId)\n }\n\n // Fetch and index records with pagination\n const pageSize = 200\n let page = 1\n let totalProcessed = 0\n let jobsEnqueued = 0\n\n for (;;) {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'fetching',\n processed: totalProcessed,\n })\n\n try {\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n page: { page, pageSize },\n skipAutoReindex: true,\n })\n\n if (!queryResult.items.length) {\n break\n }\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'indexing',\n processed: totalProcessed,\n total: queryResult.total,\n })\n\n // Build IndexableRecords for this batch\n const { records: indexableRecords, dropped } = await this.buildIndexableRecords(\n params.entityId,\n params.tenantId,\n params.organizationId ?? null,\n queryResult.items,\n config,\n )\n result.recordsDropped = (result.recordsDropped ?? 0) + dropped\n\n // Index to fulltext - either via queue or directly\n if (indexableRecords.length > 0) {\n if (params.useQueue && this.fulltextQueue) {\n // Enqueue batch for background processing - only pass minimal references\n // Worker will load fresh data from entity_indexes table\n await this.fulltextQueue.enqueue({\n jobType: 'batch-index',\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n records: indexableRecords.map((r) => ({ entityId: r.entityId, recordId: r.recordId })),\n })\n jobsEnqueued += 1\n totalProcessed += indexableRecords.length\n } else {\n // Direct indexing (blocking)\n try {\n await fulltext.bulkIndex(indexableRecords)\n totalProcessed += indexableRecords.length\n } catch (indexError) {\n // Log error but continue with remaining batches\n const errorMsg = indexError instanceof Error ? indexError.message : String(indexError)\n result.errors.push({\n entityId: params.entityId,\n error: `Batch ${page} failed: ${errorMsg}`,\n })\n }\n }\n }\n\n if (queryResult.items.length < pageSize) {\n break\n }\n page += 1\n\n // Safety check to prevent infinite loops\n if (page > MAX_PAGES) {\n break\n }\n } catch (queryError) {\n const errorMsg = queryError instanceof Error ? queryError.message : String(queryError)\n result.errors.push({\n entityId: params.entityId,\n error: `Query failed: ${errorMsg}`,\n })\n break\n }\n }\n\n result.entitiesProcessed = 1\n result.recordsIndexed = totalProcessed\n result.jobsEnqueued = jobsEnqueued\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'complete',\n processed: totalProcessed,\n total: totalProcessed,\n })\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities to fulltext search.\n *\n * When `useQueue` is true, batches are enqueued for background processing by workers.\n * When `useQueue` is false (default), batches are indexed directly (blocking).\n */\n async reindexAllToFulltext(params: ReindexAllParams): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n const fulltext = this.getFulltextStrategy()\n if (!fulltext) {\n result.success = false\n result.errors.push({ entityId: 'all' as EntityId, error: 'Fulltext strategy not available' })\n return result\n }\n\n // Recreate index once before processing all entities\n if (params.recreateIndex !== false) {\n await fulltext.recreateIndex(params.tenantId)\n }\n\n const entities = this.listEnabledEntities()\n\n for (const entityId of entities) {\n const entityResult = await this.reindexEntityToFulltext({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n recreateIndex: false, // Already recreated above\n onProgress: params.onProgress,\n useQueue: params.useQueue,\n })\n\n result.entitiesProcessed += entityResult.entitiesProcessed\n result.recordsIndexed += entityResult.recordsIndexed\n result.recordsDropped = (result.recordsDropped ?? 0) + (entityResult.recordsDropped ?? 0)\n result.jobsEnqueued = (result.jobsEnqueued ?? 0) + (entityResult.jobsEnqueued ?? 0)\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Reindex a single entity type to vector search.\n * This fetches all records from the database and enqueues them for vector indexing.\n *\n * When `useQueue` is true (default), record IDs are enqueued for background processing by workers.\n * When `useQueue` is false, records are indexed directly (blocking).\n */\n async reindexEntityToVector(params: ReindexEntityParams & { purgeFirst?: boolean }): Promise<ReindexResult> {\n searchDebug('SearchIndexer', 'reindexEntityToVector called', {\n entityId: params.entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n useQueue: params.useQueue,\n purgeFirst: params.purgeFirst,\n })\n\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n // If useQueue is requested but no queue is available, return error\n if (params.useQueue !== false && !this.vectorQueue) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Vector queue not configured for queue-based reindexing' })\n return result\n }\n\n if (!this.queryEngine) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'QueryEngine not available for reindexing' })\n return result\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Entity not configured for search' })\n return result\n }\n\n try {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'starting',\n processed: 0,\n })\n\n // Optionally purge vector index first\n if (params.purgeFirst) {\n try {\n await this.searchService.purge(params.entityId, params.tenantId, params.organizationId)\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to purge entity before vector reindex', {\n entityId: params.entityId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Fetch and enqueue records with pagination\n const pageSize = 200\n let page = 1\n let totalProcessed = 0\n let jobsEnqueued = 0\n\n for (;;) {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'fetching',\n processed: totalProcessed,\n })\n\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n page: { page, pageSize },\n skipAutoReindex: true,\n })\n\n if (!queryResult.items.length) break\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'indexing',\n processed: totalProcessed,\n total: queryResult.total,\n })\n\n // Build batch of record references\n const batchRecords: VectorBatchRecord[] = []\n for (const item of queryResult.items) {\n const recordId = String((item as Record<string, unknown>).id ?? '')\n if (!recordId) {\n result.recordsDropped = (result.recordsDropped ?? 0) + 1\n continue\n }\n batchRecords.push({\n entityId: params.entityId,\n recordId,\n })\n }\n\n // Enqueue batch for background processing or index directly\n if (batchRecords.length > 0) {\n if (params.useQueue !== false && this.vectorQueue) {\n await this.vectorQueue.enqueue({\n jobType: 'batch-index',\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n records: batchRecords,\n })\n jobsEnqueued += 1\n totalProcessed += batchRecords.length\n searchDebug('SearchIndexer', 'Enqueued batch for vector indexing', {\n entityId: params.entityId,\n batchSize: batchRecords.length,\n jobsEnqueued,\n totalProcessed,\n })\n } else {\n // Direct indexing (blocking) - index each record via SearchService\n for (const { entityId, recordId } of batchRecords) {\n try {\n await this.indexRecordById({\n entityId: entityId as EntityId,\n recordId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n })\n totalProcessed++\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to index record to vector', {\n entityId,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n }\n }\n\n if (queryResult.items.length < pageSize) break\n page += 1\n\n // Safety check to prevent infinite loops\n if (page > MAX_PAGES) {\n searchDebugWarn('SearchIndexer', 'Reached MAX_PAGES limit, stopping pagination', {\n entityId: params.entityId,\n maxPages: MAX_PAGES,\n totalProcessed,\n })\n break\n }\n }\n\n result.entitiesProcessed = 1\n result.recordsIndexed = totalProcessed\n result.jobsEnqueued = jobsEnqueued\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'complete',\n processed: totalProcessed,\n total: totalProcessed,\n })\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities to vector search.\n *\n * When `useQueue` is true (default), batches are enqueued for background processing by workers.\n * When `useQueue` is false, batches are indexed directly (blocking).\n */\n async reindexAllToVector(params: ReindexAllParams & { purgeFirst?: boolean }): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n const entities = this.listEnabledEntities()\n for (const entityId of entities) {\n const entityResult = await this.reindexEntityToVector({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n onProgress: params.onProgress,\n useQueue: params.useQueue,\n purgeFirst: params.purgeFirst,\n })\n\n result.entitiesProcessed += entityResult.entitiesProcessed\n result.recordsIndexed += entityResult.recordsIndexed\n result.recordsDropped = (result.recordsDropped ?? 0) + (entityResult.recordsDropped ?? 0)\n result.jobsEnqueued = (result.jobsEnqueued ?? 0) + (entityResult.jobsEnqueued ?? 0)\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Build IndexableRecords from raw query results.\n * Returns records and count of dropped items (missing id or other validation failures).\n */\n private async buildIndexableRecords(\n entityId: EntityId,\n tenantId: string,\n organizationId: string | null,\n items: Record<string, unknown>[],\n config: SearchEntityConfig,\n ): Promise<{ records: IndexableRecord[]; dropped: number }> {\n const records: IndexableRecord[] = []\n let dropped = 0\n\n // Debug: log first item to see structure\n if (items.length > 0) {\n searchDebug('SearchIndexer', 'Sample item structure', {\n entityId,\n sampleKeys: Object.keys(items[0]),\n sampleId: items[0].id,\n hasId: 'id' in items[0],\n firstName: items[0].first_name,\n lastName: items[0].last_name,\n preferredName: items[0].preferred_name,\n sampleItem: JSON.stringify(items[0]).slice(0, 500),\n })\n }\n\n for (const item of items) {\n const recordId = String(item.id ?? '')\n if (!recordId) {\n searchDebugWarn('SearchIndexer', 'Skipping item without id', { entityId, itemKeys: Object.keys(item) })\n dropped++\n continue\n }\n\n // Extract custom fields from record\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(item)) {\n if (key.startsWith('cf:') || key.startsWith('cf_')) {\n const cfKey = key.slice(3) // Remove 'cf:' or 'cf_' prefix (both are 3 chars)\n customFields[cfKey] = value\n }\n }\n\n const buildContext: SearchBuildContext = {\n record: item,\n customFields,\n organizationId,\n tenantId,\n queryEngine: this.noReindexQueryEngine,\n }\n\n // Try buildSource first (provides text, presenter, links, checksumSource)\n let text: string | string[] | undefined\n let presenter: SearchResultPresenter | undefined\n let url: string | undefined\n let links: SearchResultLink[] | undefined\n let checksumSource: unknown | undefined\n\n if (config.buildSource) {\n try {\n const source = await config.buildSource(buildContext)\n if (source) {\n text = source.text\n if (source.presenter) presenter = source.presenter\n if (source.links) links = source.links\n if (source.checksumSource !== undefined) checksumSource = source.checksumSource\n }\n } catch (err) {\n searchDebugWarn('SearchIndexer', 'buildSource failed', {\n entityId,\n recordId,\n error: err instanceof Error ? err.message : err,\n })\n }\n }\n\n // Fall back to formatResult if no presenter from buildSource\n if (!presenter && config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch {\n // Skip presenter on error\n }\n }\n\n // Resolve URL if not already set\n if (!url && config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch {\n // Skip URL on error\n }\n }\n\n // Resolve links if not already set\n if (!links && config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch {\n // Skip links on error\n }\n }\n\n records.push({\n entityId,\n recordId,\n tenantId,\n organizationId,\n fields: item,\n presenter,\n url,\n links,\n text,\n checksumSource,\n })\n }\n\n searchDebug('SearchIndexer', 'Finished building records', {\n entityId,\n inputCount: items.length,\n outputCount: records.length,\n dropped,\n })\n\n return { records, dropped }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAeA,SAAS,aAAa,iBAAiB,mBAAmB;AAM1D,MAAM,YAAY;AAoGX,MAAM,cAAc;AAAA,EAMzB,YACmB,eACA,eACjB,SACA;AAHiB;AACA;AAGjB,SAAK,kBAAkB,oBAAI,IAAI;AAC/B,SAAK,cAAc,SAAS;AAC5B,SAAK,gBAAgB,SAAS;AAC9B,SAAK,cAAc,SAAS;AAC5B,eAAW,gBAAgB,eAAe;AACxC,iBAAW,gBAAgB,aAAa,UAAU;AAChD,YAAI,aAAa,YAAY,OAAO;AAClC,eAAK,gBAAgB,IAAI,aAAa,UAAsB,YAAY;AAAA,QAC1E;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAY,uBAAgD;AAC1D,QAAI,CAAC,KAAK,YAAa,QAAO;AAC9B,UAAM,UAAuB;AAAA,MAC3B,OAAO,CAAC,QAAQ,SAAS,KAAK,YAAa,MAAM,QAAQ,EAAE,GAAG,MAAM,iBAAiB,KAAK,CAAC;AAAA,IAC7F;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,UAAoD;AAClE,WAAO,KAAK,gBAAgB,IAAI,QAAQ;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,sBAA4C;AAC1C,WAAO,MAAM,KAAK,KAAK,gBAAgB,OAAO,CAAC;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,UAA6B;AAC3C,UAAM,SAAS,KAAK,gBAAgB,IAAI,QAAQ;AAChD,WAAO,QAAQ,YAAY;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,QAA0C;AAC1D,UAAM,SAAS,KAAK,gBAAgB,IAAI,OAAO,QAAQ;AACvD,QAAI,CAAC,UAAU,OAAO,YAAY,OAAO;AACvC;AAAA,IACF;AAEA,UAAM,eAAmC;AAAA,MACvC,QAAQ,OAAO;AAAA,MACf,cAAc,OAAO,gBAAgB,CAAC;AAAA,MACtC,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,aAAa,KAAK;AAAA,IACpB;AAGA,QAAI;AACJ,QAAI;AACJ,QAAI;AACJ,QAAI;AACJ,QAAI;AAEJ,QAAI,OAAO,aAAa;AACtB,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,YAAY,YAAY;AACpD,YAAI,QAAQ;AACV,iBAAO,OAAO;AACd,cAAI,OAAO,UAAW,aAAY,OAAO;AACzC,cAAI,OAAO,MAAO,SAAQ,OAAO;AACjC,cAAI,OAAO,mBAAmB,OAAW,kBAAiB,OAAO;AAAA,QACnE;AAAA,MACF,SAAS,OAAO;AACd,wBAAgB,iBAAiB,sBAAsB;AAAA,UACrD,UAAU,OAAO;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI,CAAC,aAAa,OAAO,cAAc;AACrC,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,YAAI,OAAQ,aAAY;AAAA,MAC1B,SAAS,OAAO;AACd,wBAAgB,iBAAiB,uBAAuB;AAAA,UACtD,UAAU,OAAO;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI,CAAC,OAAO,OAAO,YAAY;AAC7B,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,WAAW,YAAY;AACnD,YAAI,OAAQ,OAAM;AAAA,MACpB,SAAS,OAAO;AACd,wBAAgB,iBAAiB,qBAAqB;AAAA,UACpD,UAAU,OAAO;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI,CAAC,SAAS,OAAO,cAAc;AACjC,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,YAAI,OAAQ,SAAQ;AAAA,MACtB,SAAS,OAAO;AACd,wBAAgB,iBAAiB,uBAAuB;AAAA,UACtD,UAAU,OAAO;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,UAAM,kBAAmC;AAAA,MACvC,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,QAAQ,OAAO;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,KAAK,cAAc,MAAM,eAAe;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,gBAAgB,QAK0C;AAC9D,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO,EAAE,QAAQ,WAAW,QAAQ,4BAA4B;AAAA,IAClE;AAEA,UAAM,SAAS,KAAK,gBAAgB,IAAI,OAAO,QAAQ;AACvD,QAAI,CAAC,UAAU,OAAO,YAAY,OAAO;AACvC,aAAO,EAAE,QAAQ,WAAW,QAAQ,wBAAwB;AAAA,IAC9D;AAGA,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,YAAY,MAAM,OAAO,UAAU;AAAA,QAC3D,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO,kBAAkB;AAAA,QACzC,SAAS,EAAE,IAAI,OAAO,SAAS;AAAA,QAC/B,qBAAqB;AAAA,QACrB,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE;AAAA,QAC7B,iBAAiB;AAAA,MACnB,CAAC;AAED,YAAM,SAAS,OAAO,MAAM,CAAC;AAC7B,UAAI,CAAC,QAAQ;AACX,eAAO,EAAE,QAAQ,WAAW,QAAQ,mBAAmB;AAAA,MACzD;AAGA,YAAM,eAAwC,CAAC;AAC/C,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,YAAI,IAAI,WAAW,KAAK,KAAK,IAAI,WAAW,KAAK,GAAG;AAClD,uBAAa,IAAI,MAAM,CAAC,CAAC,IAAI;AAAA,QAC/B;AAAA,MACF;AAEA,YAAM,KAAK,YAAY;AAAA,QACrB,UAAU,OAAO;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB;AAAA,QACA;AAAA,MACF,CAAC;AAED,aAAO,EAAE,QAAQ,UAAU;AAAA,IAC7B,SAAS,OAAO;AACd,kBAAY,iBAAiB,sCAAsC;AAAA,QACjE,UAAU,OAAO;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAClD,CAAC;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,QAA2C;AAC5D,UAAM,KAAK,cAAc,OAAO,OAAO,UAAU,OAAO,UAAU,OAAO,QAAQ;AAAA,EACnF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,QAA0C;AAC1D,UAAM,KAAK,cAAc,MAAM,OAAO,UAAU,OAAO,UAAU,OAAO,cAAc;AAAA,EACxF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,QAKO;AACzB,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,mBAAmB;AAAA,QACnB,gBAAgB;AAAA,QAChB,QAAQ,CAAC,EAAE,UAAU,OAAO,UAAU,OAAO,6BAA6B,CAAC;AAAA,MAC7E;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,gBAAgB,IAAI,OAAO,QAAQ;AACvD,QAAI,CAAC,UAAU,OAAO,YAAY,OAAO;AACvC,aAAO;AAAA,QACL,SAAS;AAAA,QACT,mBAAmB;AAAA,QACnB,gBAAgB;AAAA,QAChB,QAAQ,CAAC,EAAE,UAAU,OAAO,UAAU,OAAO,mCAAmC,CAAC;AAAA,MACnF;AAAA,IACF;AAEA,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,QAAQ,CAAC;AAAA,IACX;AAGA,QAAI,OAAO,YAAY;AACrB,UAAI;AACF,cAAM,KAAK,cAAc,MAAM,OAAO,UAAU,OAAO,UAAU,OAAO,cAAc;AAAA,MACxF,SAAS,OAAO;AACd,wBAAgB,iBAAiB,yCAAyC;AAAA,UACxE,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI,OAAO;AACX,UAAM,WAAW;AACjB,QAAI,UAAU;AAEd,WAAO,WAAW,QAAQ,WAAW;AACnC,UAAI;AACF,cAAM,cAAc,MAAM,KAAK,YAAY,MAAM,OAAO,UAAU;AAAA,UAChE,UAAU,OAAO;AAAA,UACjB,gBAAgB,OAAO,kBAAkB;AAAA,UACzC,qBAAqB;AAAA,UACrB,MAAM,EAAE,MAAM,SAAS;AAAA,UACvB,iBAAiB;AAAA,QACnB,CAAC;AAED,cAAM,QAAQ,YAAY;AAC1B,YAAI,MAAM,WAAW,GAAG;AACtB,oBAAU;AACV;AAAA,QACF;AAGA,cAAM,EAAE,QAAQ,IAAI,MAAM,KAAK;AAAA,UAC7B,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO,kBAAkB;AAAA,UACzB;AAAA,UACA;AAAA,QACF;AAGA,YAAI,QAAQ,SAAS,GAAG;AACtB,cAAI;AACF,kBAAM,KAAK,cAAc,UAAU,OAAO;AAC1C,mBAAO,kBAAkB,QAAQ;AAAA,UACnC,SAAS,OAAO;AACd,4BAAgB,iBAAiB,gCAAgC;AAAA,cAC/D,UAAU,OAAO;AAAA,cACjB,aAAa,QAAQ;AAAA,cACrB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,YAClD,CAAC;AAAA,UACH;AAAA,QACF;AAEA;AACA,kBAAU,MAAM,WAAW;AAAA,MAC7B,SAAS,OAAO;AACd,eAAO,UAAU;AACjB,eAAO,OAAO,KAAK;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC9D,CAAC;AACD;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAW,QAIU;AACzB,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,QAAQ,CAAC;AAAA,IACX;AAEA,UAAM,kBAAkB,KAAK,oBAAoB;AAEjD,eAAW,YAAY,iBAAiB;AACtC,YAAM,eAAe,MAAM,KAAK,cAAc;AAAA,QAC5C;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB,YAAY,OAAO;AAAA,MACrB,CAAC;AAED,aAAO;AACP,aAAO,kBAAkB,aAAa;AACtC,aAAO,OAAO,KAAK,GAAG,aAAa,MAAM;AAEzC,UAAI,CAAC,aAAa,SAAS;AACzB,eAAO,UAAU;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAiB,QAA4C;AACjE,UAAM,mBAAsC,CAAC;AAE7C,eAAW,SAAS,QAAQ;AAC1B,YAAM,SAAS,KAAK,gBAAgB,IAAI,MAAM,QAAQ;AACtD,UAAI,CAAC,UAAU,OAAO,YAAY,OAAO;AACvC;AAAA,MACF;AAEA,YAAM,eAAmC;AAAA,QACvC,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM,gBAAgB,CAAC;AAAA,QACrC,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB;AAEA,UAAI;AACJ,UAAI,OAAO,cAAc;AACvB,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,cAAI,OAAQ,aAAY;AAAA,QAC1B,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,UAAI;AACJ,UAAI,OAAO,YAAY;AACrB,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,WAAW,YAAY;AACnD,cAAI,OAAQ,OAAM;AAAA,QACpB,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,UAAI;AACJ,UAAI,OAAO,cAAc;AACvB,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,cAAI,OAAQ,SAAQ;AAAA,QACtB,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,uBAAiB,KAAK;AAAA,QACpB,UAAU,MAAM;AAAA,QAChB,UAAU,MAAM;AAAA,QAChB,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAEA,QAAI,iBAAiB,SAAS,GAAG;AAC/B,UAAI;AACF,cAAM,KAAK,cAAc,UAAU,gBAAgB;AAAA,MACrD,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,wBAAwB,iBAAiB,MAAM,aAC7C,iBAAiB,QAAQ,MAAM,UAAU,KAC3C;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,sBAAkC;AAChC,WAAO,MAAM,KAAK,KAAK,gBAAgB,KAAK,CAAC;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA,EAKQ,sBAA0D;AAChE,UAAM,WAAW,KAAK,cAAc,YAAY,UAAU;AAC1D,QAAI,CAAC,SAAU,QAAO;AACtB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,wBAAwB,QAAqD;AACjF,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,cAAc;AAAA,MACd,QAAQ,CAAC;AAAA,IACX;AAEA,UAAM,WAAW,KAAK,oBAAoB;AAC1C,QAAI,CAAC,UAAU;AACb,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,kCAAkC,CAAC;AAC1F,aAAO;AAAA,IACT;AAGA,QAAI,OAAO,YAAY,CAAC,KAAK,eAAe;AAC1C,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,2DAA2D,CAAC;AACnH,aAAO;AAAA,IACT;AAEA,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,2CAA2C,CAAC;AACnG,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,KAAK,gBAAgB,IAAI,OAAO,QAAQ;AACvD,QAAI,CAAC,QAAQ;AACX,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,mCAAmC,CAAC;AAC3F,aAAO;AAAA,IACT;AAEA,QAAI;AACF,aAAO,aAAa;AAAA,QAClB,UAAU,OAAO;AAAA,QACjB,OAAO;AAAA,QACP,WAAW;AAAA,MACb,CAAC;AAGD,UAAI,OAAO,kBAAkB,OAAO;AAClC,cAAM,SAAS,cAAc,OAAO,QAAQ;AAAA,MAC9C;AAGA,YAAM,WAAW;AACjB,UAAI,OAAO;AACX,UAAI,iBAAiB;AACrB,UAAI,eAAe;AAEnB,iBAAS;AACP,eAAO,aAAa;AAAA,UAClB,UAAU,OAAO;AAAA,UACjB,OAAO;AAAA,UACP,WAAW;AAAA,QACb,CAAC;AAED,YAAI;AACF,gBAAM,cAAc,MAAM,KAAK,YAAY,MAAM,OAAO,UAAU;AAAA,YAChE,UAAU,OAAO;AAAA,YACjB,gBAAgB,OAAO,kBAAkB;AAAA,YACzC,MAAM,EAAE,MAAM,SAAS;AAAA,YACvB,iBAAiB;AAAA,UACnB,CAAC;AAED,cAAI,CAAC,YAAY,MAAM,QAAQ;AAC7B;AAAA,UACF;AAEF,iBAAO,aAAa;AAAA,YAClB,UAAU,OAAO;AAAA,YACjB,OAAO;AAAA,YACP,WAAW;AAAA,YACX,OAAO,YAAY;AAAA,UACrB,CAAC;AAGC,gBAAM,EAAE,SAAS,kBAAkB,QAAQ,IAAI,MAAM,KAAK;AAAA,YACxD,OAAO;AAAA,YACP,OAAO;AAAA,YACP,OAAO,kBAAkB;AAAA,YACzB,YAAY;AAAA,YACZ;AAAA,UACF;AACA,iBAAO,kBAAkB,OAAO,kBAAkB,KAAK;AAGvD,cAAI,iBAAiB,SAAS,GAAG;AAC/B,gBAAI,OAAO,YAAY,KAAK,eAAe;AAGzC,oBAAM,KAAK,cAAc,QAAQ;AAAA,gBAC/B,SAAS;AAAA,gBACT,UAAU,OAAO;AAAA,gBACjB,gBAAgB,OAAO;AAAA,gBACvB,SAAS,iBAAiB,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,UAAU,EAAE,SAAS,EAAE;AAAA,cACvF,CAAC;AACD,8BAAgB;AAChB,gCAAkB,iBAAiB;AAAA,YACrC,OAAO;AAEL,kBAAI;AACF,sBAAM,SAAS,UAAU,gBAAgB;AACzC,kCAAkB,iBAAiB;AAAA,cACrC,SAAS,YAAY;AAEnB,sBAAM,WAAW,sBAAsB,QAAQ,WAAW,UAAU,OAAO,UAAU;AACrF,uBAAO,OAAO,KAAK;AAAA,kBACjB,UAAU,OAAO;AAAA,kBACjB,OAAO,SAAS,IAAI,YAAY,QAAQ;AAAA,gBAC1C,CAAC;AAAA,cACH;AAAA,YACF;AAAA,UACF;AAEA,cAAI,YAAY,MAAM,SAAS,UAAU;AACvC;AAAA,UACF;AACA,kBAAQ;AAGR,cAAI,OAAO,WAAW;AACpB;AAAA,UACF;AAAA,QACF,SAAS,YAAY;AACnB,gBAAM,WAAW,sBAAsB,QAAQ,WAAW,UAAU,OAAO,UAAU;AACrF,iBAAO,OAAO,KAAK;AAAA,YACjB,UAAU,OAAO;AAAA,YACjB,OAAO,iBAAiB,QAAQ;AAAA,UAClC,CAAC;AACD;AAAA,QACF;AAAA,MACF;AAEA,aAAO,oBAAoB;AAC3B,aAAO,iBAAiB;AACxB,aAAO,eAAe;AAEtB,aAAO,aAAa;AAAA,QAClB,UAAU,OAAO;AAAA,QACjB,OAAO;AAAA,QACP,WAAW;AAAA,QACX,OAAO;AAAA,MACT,CAAC;AAAA,IACH,SAAS,OAAO;AACd,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC9D,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,qBAAqB,QAAkD;AAC3E,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,cAAc;AAAA,MACd,QAAQ,CAAC;AAAA,IACX;AAEA,UAAM,WAAW,KAAK,oBAAoB;AAC1C,QAAI,CAAC,UAAU;AACb,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAmB,OAAO,kCAAkC,CAAC;AAC5F,aAAO;AAAA,IACT;AAGA,QAAI,OAAO,kBAAkB,OAAO;AAClC,YAAM,SAAS,cAAc,OAAO,QAAQ;AAAA,IAC9C;AAEA,UAAM,WAAW,KAAK,oBAAoB;AAE1C,eAAW,YAAY,UAAU;AAC/B,YAAM,eAAe,MAAM,KAAK,wBAAwB;AAAA,QACtD;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB,eAAe;AAAA;AAAA,QACf,YAAY,OAAO;AAAA,QACnB,UAAU,OAAO;AAAA,MACnB,CAAC;AAED,aAAO,qBAAqB,aAAa;AACzC,aAAO,kBAAkB,aAAa;AACtC,aAAO,kBAAkB,OAAO,kBAAkB,MAAM,aAAa,kBAAkB;AACvF,aAAO,gBAAgB,OAAO,gBAAgB,MAAM,aAAa,gBAAgB;AACjF,aAAO,OAAO,KAAK,GAAG,aAAa,MAAM;AAEzC,UAAI,CAAC,aAAa,SAAS;AACzB,eAAO,UAAU;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,sBAAsB,QAAgF;AAC1G,gBAAY,iBAAiB,gCAAgC;AAAA,MAC3D,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,YAAY,OAAO;AAAA,IACrB,CAAC;AAED,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,cAAc;AAAA,MACd,QAAQ,CAAC;AAAA,IACX;AAGA,QAAI,OAAO,aAAa,SAAS,CAAC,KAAK,aAAa;AAClD,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,yDAAyD,CAAC;AACjH,aAAO;AAAA,IACT;AAEA,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,2CAA2C,CAAC;AACnG,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,KAAK,gBAAgB,IAAI,OAAO,QAAQ;AACvD,QAAI,CAAC,QAAQ;AACX,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,mCAAmC,CAAC;AAC3F,aAAO;AAAA,IACT;AAEA,QAAI;AACF,aAAO,aAAa;AAAA,QAClB,UAAU,OAAO;AAAA,QACjB,OAAO;AAAA,QACP,WAAW;AAAA,MACb,CAAC;AAGD,UAAI,OAAO,YAAY;AACrB,YAAI;AACF,gBAAM,KAAK,cAAc,MAAM,OAAO,UAAU,OAAO,UAAU,OAAO,cAAc;AAAA,QACxF,SAAS,OAAO;AACd,0BAAgB,iBAAiB,gDAAgD;AAAA,YAC/E,UAAU,OAAO;AAAA,YACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,UAClD,CAAC;AAAA,QACH;AAAA,MACF;AAGA,YAAM,WAAW;AACjB,UAAI,OAAO;AACX,UAAI,iBAAiB;AACrB,UAAI,eAAe;AAEnB,iBAAS;AACP,eAAO,aAAa;AAAA,UAClB,UAAU,OAAO;AAAA,UACjB,OAAO;AAAA,UACP,WAAW;AAAA,QACb,CAAC;AAED,cAAM,cAAc,MAAM,KAAK,YAAY,MAAM,OAAO,UAAU;AAAA,UAChE,UAAU,OAAO;AAAA,UACjB,gBAAgB,OAAO,kBAAkB;AAAA,UACzC,MAAM,EAAE,MAAM,SAAS;AAAA,UACvB,iBAAiB;AAAA,QACnB,CAAC;AAED,YAAI,CAAC,YAAY,MAAM,OAAQ;AAE/B,eAAO,aAAa;AAAA,UAClB,UAAU,OAAO;AAAA,UACjB,OAAO;AAAA,UACP,WAAW;AAAA,UACX,OAAO,YAAY;AAAA,QACrB,CAAC;AAGD,cAAM,eAAoC,CAAC;AAC3C,mBAAW,QAAQ,YAAY,OAAO;AACpC,gBAAM,WAAW,OAAQ,KAAiC,MAAM,EAAE;AAClE,cAAI,CAAC,UAAU;AACb,mBAAO,kBAAkB,OAAO,kBAAkB,KAAK;AACvD;AAAA,UACF;AACA,uBAAa,KAAK;AAAA,YAChB,UAAU,OAAO;AAAA,YACjB;AAAA,UACF,CAAC;AAAA,QACH;AAGA,YAAI,aAAa,SAAS,GAAG;AAC3B,cAAI,OAAO,aAAa,SAAS,KAAK,aAAa;AACjD,kBAAM,KAAK,YAAY,QAAQ;AAAA,cAC7B,SAAS;AAAA,cACT,UAAU,OAAO;AAAA,cACjB,gBAAgB,OAAO,kBAAkB;AAAA,cACzC,SAAS;AAAA,YACX,CAAC;AACD,4BAAgB;AAChB,8BAAkB,aAAa;AAC/B,wBAAY,iBAAiB,sCAAsC;AAAA,cACjE,UAAU,OAAO;AAAA,cACjB,WAAW,aAAa;AAAA,cACxB;AAAA,cACA;AAAA,YACF,CAAC;AAAA,UACH,OAAO;AAEL,uBAAW,EAAE,UAAU,SAAS,KAAK,cAAc;AACjD,kBAAI;AACF,sBAAM,KAAK,gBAAgB;AAAA,kBACzB;AAAA,kBACA;AAAA,kBACA,UAAU,OAAO;AAAA,kBACjB,gBAAgB,OAAO;AAAA,gBACzB,CAAC;AACD;AAAA,cACF,SAAS,OAAO;AACd,gCAAgB,iBAAiB,oCAAoC;AAAA,kBACnE;AAAA,kBACA;AAAA,kBACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,gBAClD,CAAC;AAAA,cACH;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAEA,YAAI,YAAY,MAAM,SAAS,SAAU;AACzC,gBAAQ;AAGR,YAAI,OAAO,WAAW;AACpB,0BAAgB,iBAAiB,gDAAgD;AAAA,YAC/E,UAAU,OAAO;AAAA,YACjB,UAAU;AAAA,YACV;AAAA,UACF,CAAC;AACD;AAAA,QACF;AAAA,MACF;AAEA,aAAO,oBAAoB;AAC3B,aAAO,iBAAiB;AACxB,aAAO,eAAe;AAEtB,aAAO,aAAa;AAAA,QAClB,UAAU,OAAO;AAAA,QACjB,OAAO;AAAA,QACP,WAAW;AAAA,QACX,OAAO;AAAA,MACT,CAAC;AAAA,IACH,SAAS,OAAO;AACd,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC9D,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,mBAAmB,QAA6E;AACpG,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,cAAc;AAAA,MACd,QAAQ,CAAC;AAAA,IACX;AAEA,UAAM,WAAW,KAAK,oBAAoB;AAC1C,eAAW,YAAY,UAAU;AAC/B,YAAM,eAAe,MAAM,KAAK,sBAAsB;AAAA,QACpD;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB,YAAY,OAAO;AAAA,QACnB,UAAU,OAAO;AAAA,QACjB,YAAY,OAAO;AAAA,MACrB,CAAC;AAED,aAAO,qBAAqB,aAAa;AACzC,aAAO,kBAAkB,aAAa;AACtC,aAAO,kBAAkB,OAAO,kBAAkB,MAAM,aAAa,kBAAkB;AACvF,aAAO,gBAAgB,OAAO,gBAAgB,MAAM,aAAa,gBAAgB;AACjF,aAAO,OAAO,KAAK,GAAG,aAAa,MAAM;AAEzC,UAAI,CAAC,aAAa,SAAS;AACzB,eAAO,UAAU;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,sBACZ,UACA,UACA,gBACA,OACA,QAC0D;AAC1D,UAAM,UAA6B,CAAC;AACpC,QAAI,UAAU;AAGd,QAAI,MAAM,SAAS,GAAG;AACpB,kBAAY,iBAAiB,yBAAyB;AAAA,QACpD;AAAA,QACA,YAAY,OAAO,KAAK,MAAM,CAAC,CAAC;AAAA,QAChC,UAAU,MAAM,CAAC,EAAE;AAAA,QACnB,OAAO,QAAQ,MAAM,CAAC;AAAA,QACtB,WAAW,MAAM,CAAC,EAAE;AAAA,QACpB,UAAU,MAAM,CAAC,EAAE;AAAA,QACnB,eAAe,MAAM,CAAC,EAAE;AAAA,QACxB,YAAY,KAAK,UAAU,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG;AAAA,MACnD,CAAC;AAAA,IACH;AAEA,eAAW,QAAQ,OAAO;AACxB,YAAM,WAAW,OAAO,KAAK,MAAM,EAAE;AACrC,UAAI,CAAC,UAAU;AACb,wBAAgB,iBAAiB,4BAA4B,EAAE,UAAU,UAAU,OAAO,KAAK,IAAI,EAAE,CAAC;AACtG;AACA;AAAA,MACF;AAGA,YAAM,eAAwC,CAAC;AAC/C,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,YAAI,IAAI,WAAW,KAAK,KAAK,IAAI,WAAW,KAAK,GAAG;AAClD,gBAAM,QAAQ,IAAI,MAAM,CAAC;AACzB,uBAAa,KAAK,IAAI;AAAA,QACxB;AAAA,MACF;AAEA,YAAM,eAAmC;AAAA,QACvC,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA,aAAa,KAAK;AAAA,MACpB;AAGA,UAAI;AACJ,UAAI;AACJ,UAAI;AACJ,UAAI;AACJ,UAAI;AAEJ,UAAI,OAAO,aAAa;AACtB,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,YAAY,YAAY;AACpD,cAAI,QAAQ;AACV,mBAAO,OAAO;AACd,gBAAI,OAAO,UAAW,aAAY,OAAO;AACzC,gBAAI,OAAO,MAAO,SAAQ,OAAO;AACjC,gBAAI,OAAO,mBAAmB,OAAW,kBAAiB,OAAO;AAAA,UACnE;AAAA,QACF,SAAS,KAAK;AACZ,0BAAgB,iBAAiB,sBAAsB;AAAA,YACrD;AAAA,YACA;AAAA,YACA,OAAO,eAAe,QAAQ,IAAI,UAAU;AAAA,UAC9C,CAAC;AAAA,QACH;AAAA,MACF;AAGA,UAAI,CAAC,aAAa,OAAO,cAAc;AACrC,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,cAAI,OAAQ,aAAY;AAAA,QAC1B,QAAQ;AAAA,QAER;AAAA,MACF;AAGA,UAAI,CAAC,OAAO,OAAO,YAAY;AAC7B,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,WAAW,YAAY;AACnD,cAAI,OAAQ,OAAM;AAAA,QACpB,QAAQ;AAAA,QAER;AAAA,MACF;AAGA,UAAI,CAAC,SAAS,OAAO,cAAc;AACjC,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,cAAI,OAAQ,SAAQ;AAAA,QACtB,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,cAAQ,KAAK;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAEA,gBAAY,iBAAiB,6BAA6B;AAAA,MACxD;AAAA,MACA,YAAY,MAAM;AAAA,MAClB,aAAa,QAAQ;AAAA,MACrB;AAAA,IACF,CAAC;AAED,WAAO,EAAE,SAAS,QAAQ;AAAA,EAC5B;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -48,75 +48,6 @@ async function fetchDocsBatch(db, byEntityType, tenantId, organizationId) {
|
|
|
48
48
|
}
|
|
49
49
|
return allDocs;
|
|
50
50
|
}
|
|
51
|
-
function primaryNavigationHref(result) {
|
|
52
|
-
if (typeof result.url === "string" && result.url.trim().length > 0) {
|
|
53
|
-
return result.url.trim();
|
|
54
|
-
}
|
|
55
|
-
const primaryLink = result.links?.find((link) => link.kind === "primary" && link.href.trim().length > 0);
|
|
56
|
-
return primaryLink?.href.trim() ?? null;
|
|
57
|
-
}
|
|
58
|
-
function directNavigationRecordId(href) {
|
|
59
|
-
try {
|
|
60
|
-
const url = new URL(href, "http://search.local");
|
|
61
|
-
if (url.search || url.hash) return null;
|
|
62
|
-
const segments = url.pathname.split("/").filter(Boolean);
|
|
63
|
-
const lastSegment = segments.at(-1);
|
|
64
|
-
return lastSegment ? decodeURIComponent(lastSegment) : null;
|
|
65
|
-
} catch {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function presenterTitle(result) {
|
|
70
|
-
const title = result.presenter?.title?.trim();
|
|
71
|
-
return title?.length ? title : null;
|
|
72
|
-
}
|
|
73
|
-
function resultScopeKey(result, recordId) {
|
|
74
|
-
return `${result.organizationId ?? ""}:${recordId}`;
|
|
75
|
-
}
|
|
76
|
-
function mergeResultMetadata(targetMetadata, linkedMetadata) {
|
|
77
|
-
if (!targetMetadata && !linkedMetadata) return void 0;
|
|
78
|
-
return {
|
|
79
|
-
...targetMetadata,
|
|
80
|
-
...linkedMetadata
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function mergeLinkedDuplicateResults(results) {
|
|
84
|
-
const indexesByRecord = /* @__PURE__ */ new Map();
|
|
85
|
-
for (let index = 0; index < results.length; index += 1) {
|
|
86
|
-
const result = results[index];
|
|
87
|
-
const key = resultScopeKey(result, result.recordId);
|
|
88
|
-
const indexes = indexesByRecord.get(key) ?? [];
|
|
89
|
-
indexes.push(index);
|
|
90
|
-
indexesByRecord.set(key, indexes);
|
|
91
|
-
}
|
|
92
|
-
const replacements = /* @__PURE__ */ new Map();
|
|
93
|
-
const removedIndexes = /* @__PURE__ */ new Set();
|
|
94
|
-
for (let index = 0; index < results.length; index += 1) {
|
|
95
|
-
const linkedResult = results[index];
|
|
96
|
-
const href = primaryNavigationHref(linkedResult);
|
|
97
|
-
const targetRecordId = href ? directNavigationRecordId(href) : null;
|
|
98
|
-
if (!targetRecordId || targetRecordId === linkedResult.recordId) continue;
|
|
99
|
-
const targetIndexes = (indexesByRecord.get(resultScopeKey(linkedResult, targetRecordId)) ?? []).filter((candidateIndex) => candidateIndex !== index && !removedIndexes.has(candidateIndex));
|
|
100
|
-
if (targetIndexes.length !== 1) continue;
|
|
101
|
-
const targetIndex = targetIndexes[0];
|
|
102
|
-
const targetResult = replacements.get(targetIndex) ?? results[targetIndex];
|
|
103
|
-
if (primaryNavigationHref(targetResult)) continue;
|
|
104
|
-
const linkedTitle = presenterTitle(linkedResult);
|
|
105
|
-
const targetTitle = presenterTitle(targetResult);
|
|
106
|
-
if (!linkedTitle || linkedTitle !== targetTitle) continue;
|
|
107
|
-
replacements.set(targetIndex, {
|
|
108
|
-
...targetResult,
|
|
109
|
-
score: Math.max(targetResult.score, linkedResult.score),
|
|
110
|
-
source: linkedResult.score > targetResult.score ? linkedResult.source : targetResult.source,
|
|
111
|
-
presenter: linkedResult.presenter ?? targetResult.presenter,
|
|
112
|
-
url: linkedResult.url ?? targetResult.url,
|
|
113
|
-
links: linkedResult.links ?? targetResult.links,
|
|
114
|
-
metadata: mergeResultMetadata(targetResult.metadata, linkedResult.metadata)
|
|
115
|
-
});
|
|
116
|
-
removedIndexes.add(index);
|
|
117
|
-
}
|
|
118
|
-
return results.map((result, index) => replacements.get(index) ?? result).filter((_, index) => !removedIndexes.has(index)).sort((left, right) => right.score - left.score);
|
|
119
|
-
}
|
|
120
51
|
async function computePresenterAndLinks(doc, entityId, recordId, config, tenantId, organizationId, queryEngine) {
|
|
121
52
|
let presenter = null;
|
|
122
53
|
let url;
|
|
@@ -230,7 +161,7 @@ function createPresenterEnricher(db, entityConfigMap, queryEngine, encryptionSer
|
|
|
230
161
|
for (const { key, presenter, url, links } of computed) {
|
|
231
162
|
enrichmentMap.set(key, { presenter, url, links });
|
|
232
163
|
}
|
|
233
|
-
|
|
164
|
+
return results.map((result) => {
|
|
234
165
|
if (!shouldEnrich(result)) return result;
|
|
235
166
|
const key = `${result.entityId}:${result.recordId}`;
|
|
236
167
|
const enriched = enrichmentMap.get(key);
|
|
@@ -242,7 +173,6 @@ function createPresenterEnricher(db, entityConfigMap, queryEngine, encryptionSer
|
|
|
242
173
|
links: enriched.links ?? result.links
|
|
243
174
|
};
|
|
244
175
|
});
|
|
245
|
-
return mergeLinkedDuplicateResults(enrichedResults);
|
|
246
176
|
};
|
|
247
177
|
}
|
|
248
178
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/presenter-enricher.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Kysely } from 'kysely'\nimport type {\n SearchBuildContext,\n SearchResult,\n SearchResultPresenter,\n SearchResultLink,\n SearchEntityConfig,\n PresenterEnricherFn,\n} from '../types'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { decryptIndexDocForSearch } from '@open-mercato/shared/lib/encryption/indexDoc'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { extractFallbackPresenter } from './fallback-presenter'\nimport { needsSearchResultEnrichment } from './search-result-enrichment'\n\n/** Maximum number of record IDs per batch query to avoid hitting DB parameter limits */\nconst BATCH_SIZE = 500\n\n/** Diagnostic logger - surfaces issues without breaking flow, gated by OM_LOG_LEVEL=debug */\nconst enricherLogger = createLogger('search').child({ component: 'presenter-enricher' })\nconst logWarning = (message: string, context?: Record<string, unknown>) => {\n enricherLogger.debug(message, context)\n}\n\n/**\n * Split an array into chunks of specified size.\n */\nfunction chunk<T>(array: T[], size: number): T[][] {\n const chunks: T[][] = []\n for (let i = 0; i < array.length; i += size) {\n chunks.push(array.slice(i, i + size))\n }\n return chunks\n}\n\n/**\n * Build a single batch query for multiple entity types and their record IDs.\n * Uses OR conditions to fetch all needed docs in one round trip.\n */\nasync function fetchDocsBatch(\n db: Kysely<any>,\n byEntityType: Map<string, SearchResult[]>,\n tenantId: string,\n organizationId?: string | null,\n): Promise<Array<{ entity_type: string; entity_id: string; doc: Record<string, unknown> }>> {\n const allDocs: Array<{ entity_type: string; entity_id: string; doc: Record<string, unknown> }> = []\n\n // Collect all entity type + record ID pairs\n const allPairs: Array<{ entityType: string; recordId: string }> = []\n for (const [entityType, results] of byEntityType) {\n for (const result of results) {\n allPairs.push({ entityType, recordId: result.recordId })\n }\n }\n\n if (allPairs.length === 0) return allDocs\n\n // Process in chunks to avoid hitting DB parameter limits\n const chunks = chunk(allPairs, BATCH_SIZE)\n\n for (const pairChunk of chunks) {\n // Group by entity type within this chunk for efficient OR query\n const chunkByType = new Map<string, string[]>()\n for (const { entityType, recordId } of pairChunk) {\n const ids = chunkByType.get(entityType) ?? []\n ids.push(recordId)\n chunkByType.set(entityType, ids)\n }\n\n // Build query with OR conditions per entity type\n let query = db\n .selectFrom('entity_indexes' as any)\n .select(['entity_type' as any, 'entity_id' as any, 'doc' as any])\n .where('tenant_id' as any, '=', tenantId)\n .where('deleted_at' as any, 'is', null)\n .where((eb: any) => eb.or(\n Array.from(chunkByType.entries()).map(([entityType, recordIds]) => eb.and([\n eb('entity_type' as any, '=', entityType),\n eb('entity_id' as any, 'in', recordIds),\n ])),\n ))\n\n // Add organization filter if provided\n if (organizationId) {\n query = query.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', organizationId),\n eb('organization_id' as any, 'is', null),\n ]))\n }\n\n const rows = await query.execute() as Array<{ entity_type: string; entity_id: string; doc: Record<string, unknown> }>\n allDocs.push(...rows)\n }\n\n return allDocs\n}\n\n/** Result type for presenter and links computation */\ntype EnrichmentResult = {\n presenter: SearchResultPresenter | null\n url?: string\n links?: SearchResultLink[]\n}\n\nfunction primaryNavigationHref(result: SearchResult): string | null {\n if (typeof result.url === 'string' && result.url.trim().length > 0) {\n return result.url.trim()\n }\n const primaryLink = result.links?.find((link) => link.kind === 'primary' && link.href.trim().length > 0)\n return primaryLink?.href.trim() ?? null\n}\n\nfunction directNavigationRecordId(href: string): string | null {\n try {\n const url = new URL(href, 'http://search.local')\n if (url.search || url.hash) return null\n const segments = url.pathname.split('/').filter(Boolean)\n const lastSegment = segments.at(-1)\n return lastSegment ? decodeURIComponent(lastSegment) : null\n } catch {\n return null\n }\n}\n\nfunction presenterTitle(result: SearchResult): string | null {\n const title = result.presenter?.title?.trim()\n return title?.length ? title : null\n}\n\nfunction resultScopeKey(result: SearchResult, recordId: string): string {\n return `${result.organizationId ?? ''}:${recordId}`\n}\n\nfunction mergeResultMetadata(\n targetMetadata: SearchResult['metadata'],\n linkedMetadata: SearchResult['metadata'],\n): SearchResult['metadata'] {\n if (!targetMetadata && !linkedMetadata) return undefined\n return {\n ...targetMetadata,\n ...linkedMetadata,\n }\n}\n\nfunction mergeLinkedDuplicateResults(results: SearchResult[]): SearchResult[] {\n const indexesByRecord = new Map<string, number[]>()\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index]\n const key = resultScopeKey(result, result.recordId)\n const indexes = indexesByRecord.get(key) ?? []\n indexes.push(index)\n indexesByRecord.set(key, indexes)\n }\n\n const replacements = new Map<number, SearchResult>()\n const removedIndexes = new Set<number>()\n\n for (let index = 0; index < results.length; index += 1) {\n const linkedResult = results[index]\n const href = primaryNavigationHref(linkedResult)\n const targetRecordId = href ? directNavigationRecordId(href) : null\n if (!targetRecordId || targetRecordId === linkedResult.recordId) continue\n\n const targetIndexes = (indexesByRecord.get(resultScopeKey(linkedResult, targetRecordId)) ?? [])\n .filter((candidateIndex) => candidateIndex !== index && !removedIndexes.has(candidateIndex))\n if (targetIndexes.length !== 1) continue\n\n const targetIndex = targetIndexes[0]\n const targetResult = replacements.get(targetIndex) ?? results[targetIndex]\n if (primaryNavigationHref(targetResult)) continue\n\n const linkedTitle = presenterTitle(linkedResult)\n const targetTitle = presenterTitle(targetResult)\n if (!linkedTitle || linkedTitle !== targetTitle) continue\n\n replacements.set(targetIndex, {\n ...targetResult,\n score: Math.max(targetResult.score, linkedResult.score),\n source: linkedResult.score > targetResult.score ? linkedResult.source : targetResult.source,\n presenter: linkedResult.presenter ?? targetResult.presenter,\n url: linkedResult.url ?? targetResult.url,\n links: linkedResult.links ?? targetResult.links,\n metadata: mergeResultMetadata(targetResult.metadata, linkedResult.metadata),\n })\n removedIndexes.add(index)\n }\n\n return results\n .map((result, index) => replacements.get(index) ?? result)\n .filter((_, index) => !removedIndexes.has(index))\n .sort((left, right) => right.score - left.score)\n}\n\n/**\n * Compute presenter, URL, and links for a single doc using config or fallback.\n * Returns presenter (null if cannot be computed), and optionally URL/links from config.\n */\nasync function computePresenterAndLinks(\n doc: Record<string, unknown>,\n entityId: string,\n recordId: string,\n config: SearchEntityConfig | undefined,\n tenantId: string,\n organizationId: string | null | undefined,\n queryEngine: QueryEngine | undefined,\n): Promise<EnrichmentResult> {\n let presenter: SearchResultPresenter | null = null\n let url: string | undefined\n let links: SearchResultLink[] | undefined\n\n // Build context for config functions\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(doc)) {\n if (key.startsWith('cf:') || key.startsWith('cf_')) {\n customFields[key.slice(3)] = value\n }\n }\n\n const buildContext: SearchBuildContext = {\n record: doc,\n customFields,\n organizationId,\n tenantId,\n queryEngine,\n }\n\n if (config?.formatResult || config?.buildSource) {\n if (config.formatResult) {\n try {\n presenter = (await config.formatResult(buildContext)) ?? null\n } catch (err) {\n logWarning('formatResult failed', { entityId, recordId, err })\n }\n }\n\n if (!presenter && config.buildSource) {\n try {\n const source = await config.buildSource(buildContext)\n if (source?.presenter) presenter = source.presenter\n if (source?.links) links = source.links\n } catch (err) {\n logWarning('buildSource failed', { entityId, recordId, err })\n }\n }\n }\n\n // Fallback presenter: extract from doc fields directly\n if (!presenter) {\n presenter = extractFallbackPresenter(doc, entityId, recordId)\n }\n\n // Resolve URL from config\n if (config?.resolveUrl) {\n try {\n url = (await config.resolveUrl(buildContext)) ?? undefined\n } catch {\n // Skip URL resolution errors\n }\n }\n\n // Resolve links from config (if not already set from buildSource)\n if (!links && config?.resolveLinks) {\n try {\n links = (await config.resolveLinks(buildContext)) ?? undefined\n } catch {\n // Skip link resolution errors\n }\n }\n\n return { presenter, url, links }\n}\n\n/**\n * Create a presenter enricher that loads data from entity_indexes and computes presenter.\n * Uses formatResult from search.ts configs when available, otherwise falls back to extracting\n * common fields like display_name, name, title from the doc.\n *\n * Optimizations:\n * - Single batch DB query for all entity types (instead of one per type)\n * - Parallel Promise.all for formatResult/buildSource calls\n * - Tenant/organization scoping for security\n * - Chunked queries to avoid DB parameter limits\n * - Automatic decryption of encrypted fields when encryption service is provided\n */\nexport function createPresenterEnricher(\n db: Kysely<any>,\n entityConfigMap: Map<EntityId, SearchEntityConfig>,\n queryEngine?: QueryEngine,\n encryptionService?: TenantDataEncryptionService | null,\n): PresenterEnricherFn {\n return async (results, tenantId, organizationId) => {\n const shouldEnrich = (result: SearchResult): boolean =>\n needsSearchResultEnrichment(result) || entityConfigMap.has(result.entityId as EntityId)\n\n const missingResults = results.filter(shouldEnrich)\n if (missingResults.length === 0) return results\n\n // Group by entity type for config lookup\n const byEntityType = new Map<string, SearchResult[]>()\n for (const result of missingResults) {\n const group = byEntityType.get(result.entityId) ?? []\n group.push(result)\n byEntityType.set(result.entityId, group)\n }\n\n // Single batch query for all docs across all entity types\n const rawDocs = await fetchDocsBatch(db, byEntityType, tenantId, organizationId)\n\n // Decrypt docs in parallel using DEK cache for efficiency\n const dekCache = new Map<string | null, string | null>()\n\n const decryptedDocs = await Promise.all(\n rawDocs.map(async (row) => {\n try {\n // Use organization_id from the doc itself for proper encryption map lookup\n // This is critical for global search where organizationId param is null\n const docData = row.doc as Record<string, unknown>\n const docOrgId = (docData.organization_id as string | null | undefined) ?? organizationId\n const scope = { tenantId, organizationId: docOrgId }\n\n const decryptedDoc = await decryptIndexDocForSearch(\n row.entity_type,\n row.doc,\n scope,\n encryptionService ?? null,\n dekCache,\n )\n return { ...row, doc: decryptedDoc }\n } catch (err) {\n logWarning('Failed to decrypt doc', { entityId: row.entity_type, recordId: row.entity_id, err })\n return row // Return original doc if decryption fails\n }\n }),\n )\n\n // Build doc lookup map for fast access\n const docMap = new Map<string, Record<string, unknown>>()\n for (const row of decryptedDocs) {\n docMap.set(`${row.entity_type}:${row.entity_id}`, row.doc)\n }\n\n // Compute presenters and links in parallel\n const enrichmentPromises = missingResults.map(async (result) => {\n const key = `${result.entityId}:${result.recordId}`\n const doc = docMap.get(key)\n\n if (!doc) {\n logWarning('Doc not found in entity_indexes', { entityId: result.entityId, recordId: result.recordId })\n return { key, presenter: null, url: undefined, links: undefined }\n }\n\n const config = entityConfigMap.get(result.entityId as EntityId)\n const enrichment = await computePresenterAndLinks(\n doc,\n result.entityId,\n result.recordId,\n config,\n tenantId,\n organizationId,\n queryEngine,\n )\n\n return { key, ...enrichment }\n })\n\n const computed = await Promise.all(enrichmentPromises)\n\n // Build enrichment map from parallel results\n const enrichmentMap = new Map<string, EnrichmentResult>()\n for (const { key, presenter, url, links } of computed) {\n enrichmentMap.set(key, { presenter, url, links })\n }\n\n // Enrich results with computed presenter, URL, and links\n const enrichedResults = results.map((result) => {\n if (!shouldEnrich(result)) return result\n const key = `${result.entityId}:${result.recordId}`\n const enriched = enrichmentMap.get(key)\n if (!enriched) return result\n return {\n ...result,\n presenter: enriched.presenter ?? result.presenter,\n url: result.url ?? enriched.url,\n links: enriched.links ?? result.links,\n }\n })\n\n return mergeLinkedDuplicateResults(enrichedResults)\n }\n}\n"],
|
|
5
|
-
"mappings": "AAYA,SAAS,gCAAgC;AACzC,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC;AACzC,SAAS,mCAAmC;AAG5C,MAAM,aAAa;AAGnB,MAAM,iBAAiB,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,qBAAqB,CAAC;AACvF,MAAM,aAAa,CAAC,SAAiB,YAAsC;AACzE,iBAAe,MAAM,SAAS,OAAO;AACvC;AAKA,SAAS,MAAS,OAAY,MAAqB;AACjD,QAAM,SAAgB,CAAC;AACvB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,MAAM;AAC3C,WAAO,KAAK,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC;AAAA,EACtC;AACA,SAAO;AACT;AAMA,eAAe,eACb,IACA,cACA,UACA,gBAC0F;AAC1F,QAAM,UAA2F,CAAC;AAGlG,QAAM,WAA4D,CAAC;AACnE,aAAW,CAAC,YAAY,OAAO,KAAK,cAAc;AAChD,eAAW,UAAU,SAAS;AAC5B,eAAS,KAAK,EAAE,YAAY,UAAU,OAAO,SAAS,CAAC;AAAA,IACzD;AAAA,EACF;AAEA,MAAI,SAAS,WAAW,EAAG,QAAO;AAGlC,QAAM,SAAS,MAAM,UAAU,UAAU;AAEzC,aAAW,aAAa,QAAQ;AAE9B,UAAM,cAAc,oBAAI,IAAsB;AAC9C,eAAW,EAAE,YAAY,SAAS,KAAK,WAAW;AAChD,YAAM,MAAM,YAAY,IAAI,UAAU,KAAK,CAAC;AAC5C,UAAI,KAAK,QAAQ;AACjB,kBAAY,IAAI,YAAY,GAAG;AAAA,IACjC;AAGA,QAAI,QAAQ,GACT,WAAW,gBAAuB,EAClC,OAAO,CAAC,eAAsB,aAAoB,KAAY,CAAC,EAC/D,MAAM,aAAoB,KAAK,QAAQ,EACvC,MAAM,cAAqB,MAAM,IAAI,EACrC,MAAM,CAAC,OAAY,GAAG;AAAA,MACrB,MAAM,KAAK,YAAY,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,YAAY,SAAS,MAAM,GAAG,IAAI;AAAA,QACxE,GAAG,eAAsB,KAAK,UAAU;AAAA,QACxC,GAAG,aAAoB,MAAM,SAAS;AAAA,MACxC,CAAC,CAAC;AAAA,IACJ,CAAC;AAGH,QAAI,gBAAgB;AAClB,cAAQ,MAAM,MAAM,CAAC,OAAY,GAAG,GAAG;AAAA,QACrC,GAAG,mBAA0B,KAAK,cAAc;AAAA,QAChD,GAAG,mBAA0B,MAAM,IAAI;AAAA,MACzC,CAAC,CAAC;AAAA,IACJ;AAEA,UAAM,OAAO,MAAM,MAAM,QAAQ;AACjC,YAAQ,KAAK,GAAG,IAAI;AAAA,EACtB;AAEA,SAAO;AACT;
|
|
4
|
+
"sourcesContent": ["import type { Kysely } from 'kysely'\nimport type {\n SearchBuildContext,\n SearchResult,\n SearchResultPresenter,\n SearchResultLink,\n SearchEntityConfig,\n PresenterEnricherFn,\n} from '../types'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { decryptIndexDocForSearch } from '@open-mercato/shared/lib/encryption/indexDoc'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { extractFallbackPresenter } from './fallback-presenter'\nimport { needsSearchResultEnrichment } from './search-result-enrichment'\n\n/** Maximum number of record IDs per batch query to avoid hitting DB parameter limits */\nconst BATCH_SIZE = 500\n\n/** Diagnostic logger - surfaces issues without breaking flow, gated by OM_LOG_LEVEL=debug */\nconst enricherLogger = createLogger('search').child({ component: 'presenter-enricher' })\nconst logWarning = (message: string, context?: Record<string, unknown>) => {\n enricherLogger.debug(message, context)\n}\n\n/**\n * Split an array into chunks of specified size.\n */\nfunction chunk<T>(array: T[], size: number): T[][] {\n const chunks: T[][] = []\n for (let i = 0; i < array.length; i += size) {\n chunks.push(array.slice(i, i + size))\n }\n return chunks\n}\n\n/**\n * Build a single batch query for multiple entity types and their record IDs.\n * Uses OR conditions to fetch all needed docs in one round trip.\n */\nasync function fetchDocsBatch(\n db: Kysely<any>,\n byEntityType: Map<string, SearchResult[]>,\n tenantId: string,\n organizationId?: string | null,\n): Promise<Array<{ entity_type: string; entity_id: string; doc: Record<string, unknown> }>> {\n const allDocs: Array<{ entity_type: string; entity_id: string; doc: Record<string, unknown> }> = []\n\n // Collect all entity type + record ID pairs\n const allPairs: Array<{ entityType: string; recordId: string }> = []\n for (const [entityType, results] of byEntityType) {\n for (const result of results) {\n allPairs.push({ entityType, recordId: result.recordId })\n }\n }\n\n if (allPairs.length === 0) return allDocs\n\n // Process in chunks to avoid hitting DB parameter limits\n const chunks = chunk(allPairs, BATCH_SIZE)\n\n for (const pairChunk of chunks) {\n // Group by entity type within this chunk for efficient OR query\n const chunkByType = new Map<string, string[]>()\n for (const { entityType, recordId } of pairChunk) {\n const ids = chunkByType.get(entityType) ?? []\n ids.push(recordId)\n chunkByType.set(entityType, ids)\n }\n\n // Build query with OR conditions per entity type\n let query = db\n .selectFrom('entity_indexes' as any)\n .select(['entity_type' as any, 'entity_id' as any, 'doc' as any])\n .where('tenant_id' as any, '=', tenantId)\n .where('deleted_at' as any, 'is', null)\n .where((eb: any) => eb.or(\n Array.from(chunkByType.entries()).map(([entityType, recordIds]) => eb.and([\n eb('entity_type' as any, '=', entityType),\n eb('entity_id' as any, 'in', recordIds),\n ])),\n ))\n\n // Add organization filter if provided\n if (organizationId) {\n query = query.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', organizationId),\n eb('organization_id' as any, 'is', null),\n ]))\n }\n\n const rows = await query.execute() as Array<{ entity_type: string; entity_id: string; doc: Record<string, unknown> }>\n allDocs.push(...rows)\n }\n\n return allDocs\n}\n\n/** Result type for presenter and links computation */\ntype EnrichmentResult = {\n presenter: SearchResultPresenter | null\n url?: string\n links?: SearchResultLink[]\n}\n\n/**\n * Compute presenter, URL, and links for a single doc using config or fallback.\n * Returns presenter (null if cannot be computed), and optionally URL/links from config.\n */\nasync function computePresenterAndLinks(\n doc: Record<string, unknown>,\n entityId: string,\n recordId: string,\n config: SearchEntityConfig | undefined,\n tenantId: string,\n organizationId: string | null | undefined,\n queryEngine: QueryEngine | undefined,\n): Promise<EnrichmentResult> {\n let presenter: SearchResultPresenter | null = null\n let url: string | undefined\n let links: SearchResultLink[] | undefined\n\n // Build context for config functions\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(doc)) {\n if (key.startsWith('cf:') || key.startsWith('cf_')) {\n customFields[key.slice(3)] = value\n }\n }\n\n const buildContext: SearchBuildContext = {\n record: doc,\n customFields,\n organizationId,\n tenantId,\n queryEngine,\n }\n\n if (config?.formatResult || config?.buildSource) {\n if (config.formatResult) {\n try {\n presenter = (await config.formatResult(buildContext)) ?? null\n } catch (err) {\n logWarning('formatResult failed', { entityId, recordId, err })\n }\n }\n\n if (!presenter && config.buildSource) {\n try {\n const source = await config.buildSource(buildContext)\n if (source?.presenter) presenter = source.presenter\n if (source?.links) links = source.links\n } catch (err) {\n logWarning('buildSource failed', { entityId, recordId, err })\n }\n }\n }\n\n // Fallback presenter: extract from doc fields directly\n if (!presenter) {\n presenter = extractFallbackPresenter(doc, entityId, recordId)\n }\n\n // Resolve URL from config\n if (config?.resolveUrl) {\n try {\n url = (await config.resolveUrl(buildContext)) ?? undefined\n } catch {\n // Skip URL resolution errors\n }\n }\n\n // Resolve links from config (if not already set from buildSource)\n if (!links && config?.resolveLinks) {\n try {\n links = (await config.resolveLinks(buildContext)) ?? undefined\n } catch {\n // Skip link resolution errors\n }\n }\n\n return { presenter, url, links }\n}\n\n/**\n * Create a presenter enricher that loads data from entity_indexes and computes presenter.\n * Uses formatResult from search.ts configs when available, otherwise falls back to extracting\n * common fields like display_name, name, title from the doc.\n *\n * Optimizations:\n * - Single batch DB query for all entity types (instead of one per type)\n * - Parallel Promise.all for formatResult/buildSource calls\n * - Tenant/organization scoping for security\n * - Chunked queries to avoid DB parameter limits\n * - Automatic decryption of encrypted fields when encryption service is provided\n */\nexport function createPresenterEnricher(\n db: Kysely<any>,\n entityConfigMap: Map<EntityId, SearchEntityConfig>,\n queryEngine?: QueryEngine,\n encryptionService?: TenantDataEncryptionService | null,\n): PresenterEnricherFn {\n return async (results, tenantId, organizationId) => {\n const shouldEnrich = (result: SearchResult): boolean =>\n needsSearchResultEnrichment(result) || entityConfigMap.has(result.entityId as EntityId)\n\n const missingResults = results.filter(shouldEnrich)\n if (missingResults.length === 0) return results\n\n // Group by entity type for config lookup\n const byEntityType = new Map<string, SearchResult[]>()\n for (const result of missingResults) {\n const group = byEntityType.get(result.entityId) ?? []\n group.push(result)\n byEntityType.set(result.entityId, group)\n }\n\n // Single batch query for all docs across all entity types\n const rawDocs = await fetchDocsBatch(db, byEntityType, tenantId, organizationId)\n\n // Decrypt docs in parallel using DEK cache for efficiency\n const dekCache = new Map<string | null, string | null>()\n\n const decryptedDocs = await Promise.all(\n rawDocs.map(async (row) => {\n try {\n // Use organization_id from the doc itself for proper encryption map lookup\n // This is critical for global search where organizationId param is null\n const docData = row.doc as Record<string, unknown>\n const docOrgId = (docData.organization_id as string | null | undefined) ?? organizationId\n const scope = { tenantId, organizationId: docOrgId }\n\n const decryptedDoc = await decryptIndexDocForSearch(\n row.entity_type,\n row.doc,\n scope,\n encryptionService ?? null,\n dekCache,\n )\n return { ...row, doc: decryptedDoc }\n } catch (err) {\n logWarning('Failed to decrypt doc', { entityId: row.entity_type, recordId: row.entity_id, err })\n return row // Return original doc if decryption fails\n }\n }),\n )\n\n // Build doc lookup map for fast access\n const docMap = new Map<string, Record<string, unknown>>()\n for (const row of decryptedDocs) {\n docMap.set(`${row.entity_type}:${row.entity_id}`, row.doc)\n }\n\n // Compute presenters and links in parallel\n const enrichmentPromises = missingResults.map(async (result) => {\n const key = `${result.entityId}:${result.recordId}`\n const doc = docMap.get(key)\n\n if (!doc) {\n logWarning('Doc not found in entity_indexes', { entityId: result.entityId, recordId: result.recordId })\n return { key, presenter: null, url: undefined, links: undefined }\n }\n\n const config = entityConfigMap.get(result.entityId as EntityId)\n const enrichment = await computePresenterAndLinks(\n doc,\n result.entityId,\n result.recordId,\n config,\n tenantId,\n organizationId,\n queryEngine,\n )\n\n return { key, ...enrichment }\n })\n\n const computed = await Promise.all(enrichmentPromises)\n\n // Build enrichment map from parallel results\n const enrichmentMap = new Map<string, EnrichmentResult>()\n for (const { key, presenter, url, links } of computed) {\n enrichmentMap.set(key, { presenter, url, links })\n }\n\n // Enrich results with computed presenter, URL, and links\n return results.map((result) => {\n if (!shouldEnrich(result)) return result\n const key = `${result.entityId}:${result.recordId}`\n const enriched = enrichmentMap.get(key)\n if (!enriched) return result\n return {\n ...result,\n presenter: enriched.presenter ?? result.presenter,\n url: result.url ?? enriched.url,\n links: enriched.links ?? result.links,\n }\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|