@open-mercato/search 0.6.4-develop.4239.1.4a264a5828 → 0.6.4-develop.4264.1.53368d85fe

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/fulltext/drivers/meilisearch/index.js +14 -2
  2. package/dist/fulltext/drivers/meilisearch/index.js.map +2 -2
  3. package/dist/lib/merger.js +10 -3
  4. package/dist/lib/merger.js.map +2 -2
  5. package/dist/modules/search/api/search/global/route.js +3 -0
  6. package/dist/modules/search/api/search/global/route.js.map +2 -2
  7. package/dist/modules/search/api/search/route.js +3 -0
  8. package/dist/modules/search/api/search/route.js.map +2 -2
  9. package/dist/service.js +25 -1
  10. package/dist/service.js.map +2 -2
  11. package/dist/strategies/fulltext.strategy.js +6 -0
  12. package/dist/strategies/fulltext.strategy.js.map +2 -2
  13. package/dist/strategies/token.strategy.js +16 -4
  14. package/dist/strategies/token.strategy.js.map +2 -2
  15. package/dist/strategies/vector.strategy.js +16 -2
  16. package/dist/strategies/vector.strategy.js.map +2 -2
  17. package/dist/vector/drivers/pgvector/index.js +8 -2
  18. package/dist/vector/drivers/pgvector/index.js.map +2 -2
  19. package/dist/vector/types.js.map +2 -2
  20. package/package.json +4 -4
  21. package/src/__tests__/service.test.ts +44 -0
  22. package/src/fulltext/drivers/meilisearch/index.ts +16 -2
  23. package/src/fulltext/types.ts +2 -0
  24. package/src/lib/merger.ts +9 -2
  25. package/src/modules/search/api/__tests__/org-scoping.routes.test.ts +29 -1
  26. package/src/modules/search/api/search/global/route.ts +3 -0
  27. package/src/modules/search/api/search/route.ts +3 -0
  28. package/src/service.ts +32 -1
  29. package/src/strategies/fulltext.strategy.ts +9 -0
  30. package/src/strategies/token.strategy.ts +25 -4
  31. package/src/strategies/vector.strategy.ts +21 -3
  32. package/src/vector/drivers/pgvector/index.ts +14 -2
  33. package/src/vector/types.ts +1 -0
@@ -1,4 +1,12 @@
1
1
  import { sql } from "kysely";
2
+ function normalizeOrganizationIds(options) {
3
+ const single = typeof options.organizationId === "string" ? options.organizationId.trim() : "";
4
+ if (single) return [single];
5
+ if (!Array.isArray(options.organizationIds)) return null;
6
+ return Array.from(new Set(
7
+ options.organizationIds.map((value) => typeof value === "string" ? value.trim() : "").filter((value) => value.length > 0)
8
+ ));
9
+ }
2
10
  class TokenSearchStrategy {
3
11
  constructor(db, config) {
4
12
  this.db = db;
@@ -14,6 +22,8 @@ class TokenSearchStrategy {
14
22
  async ensureReady() {
15
23
  }
16
24
  async search(query, options) {
25
+ const organizationIds = normalizeOrganizationIds(options);
26
+ if (organizationIds && organizationIds.length === 0) return [];
17
27
  const { tokenizeText } = await import("@open-mercato/shared/lib/search/tokenize");
18
28
  const { resolveSearchConfig } = await import("@open-mercato/shared/lib/search/config");
19
29
  const config = resolveSearchConfig();
@@ -25,10 +35,11 @@ class TokenSearchStrategy {
25
35
  let queryBuilder = this.db.selectFrom("search_tokens").select([
26
36
  "entity_type",
27
37
  "entity_id",
38
+ "organization_id",
28
39
  sql`count(*)`.as("match_count")
29
- ]).where("token_hash", "in", hashes).where("tenant_id", "=", options.tenantId).groupBy(["entity_type", "entity_id"]).having(sql`count(distinct token_hash) >= ${minMatches}`).orderBy(sql`count(distinct token_hash) desc`).limit(limit);
30
- if (options.organizationId) {
31
- queryBuilder = queryBuilder.where("organization_id", "=", options.organizationId);
40
+ ]).where("token_hash", "in", hashes).where("tenant_id", "=", options.tenantId).groupBy(["entity_type", "entity_id", "organization_id"]).having(sql`count(distinct token_hash) >= ${minMatches}`).orderBy(sql`count(distinct token_hash) desc`).limit(limit);
41
+ if (organizationIds) {
42
+ queryBuilder = queryBuilder.where("organization_id", "in", organizationIds);
32
43
  }
33
44
  if (options.entityTypes?.length) {
34
45
  queryBuilder = queryBuilder.where("entity_type", "in", options.entityTypes);
@@ -41,7 +52,8 @@ class TokenSearchStrategy {
41
52
  entityId: row.entity_type,
42
53
  recordId: row.entity_id,
43
54
  score,
44
- source: this.id
55
+ source: this.id,
56
+ organizationId: row.organization_id ?? null
45
57
  };
46
58
  });
47
59
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/strategies/token.strategy.ts"],
4
- "sourcesContent": ["import { type Kysely, sql, type SqlBool } from 'kysely'\nimport type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n IndexableRecord,\n} from '../types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\n\n/**\n * Configuration for TokenSearchStrategy.\n */\nexport type TokenStrategyConfig = {\n /** Minimum number of query tokens that must match (0-1 ratio, default 0.5) */\n minMatchRatio?: number\n /** Default limit for search results */\n defaultLimit?: number\n}\n\n/**\n * TokenSearchStrategy provides hash-based search using the existing search_tokens table.\n * This strategy is always available and serves as a fallback when other strategies fail.\n *\n * It tokenizes queries into hashes and matches against pre-indexed token hashes,\n * enabling search on encrypted fields without exposing plaintext to external services.\n */\nexport class TokenSearchStrategy implements SearchStrategy {\n readonly id: SearchStrategyId = 'tokens'\n readonly name = 'Token Search'\n readonly priority = 10 // Lowest priority, always available as fallback\n\n private readonly minMatchRatio: number\n private readonly defaultLimit: number\n\n constructor(\n private readonly db: Kysely<any>,\n config?: TokenStrategyConfig,\n ) {\n this.minMatchRatio = config?.minMatchRatio ?? 0.5\n this.defaultLimit = config?.defaultLimit ?? 50\n }\n\n async isAvailable(): Promise<boolean> {\n return true // Always available\n }\n\n async ensureReady(): Promise<void> {\n // No initialization needed\n }\n\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n // Dynamically import tokenization to avoid circular dependencies\n const { tokenizeText } = await import('@open-mercato/shared/lib/search/tokenize')\n const { resolveSearchConfig } = await import('@open-mercato/shared/lib/search/config')\n\n const config = resolveSearchConfig()\n if (!config.enabled) return []\n\n const { hashes } = tokenizeText(query, config)\n if (hashes.length === 0) return []\n\n const minMatches = Math.max(1, Math.ceil(hashes.length * this.minMatchRatio))\n const limit = options.limit ?? this.defaultLimit\n\n let queryBuilder = this.db\n .selectFrom('search_tokens' as any)\n .select([\n 'entity_type' as any,\n 'entity_id' as any,\n sql<string>`count(*)`.as('match_count'),\n ])\n .where('token_hash' as any, 'in', hashes)\n .where('tenant_id' as any, '=', options.tenantId)\n .groupBy(['entity_type' as any, 'entity_id' as any])\n .having(sql<SqlBool>`count(distinct token_hash) >= ${minMatches}`)\n .orderBy(sql`count(distinct token_hash) desc`)\n .limit(limit)\n\n if (options.organizationId) {\n queryBuilder = queryBuilder.where('organization_id' as any, '=', options.organizationId)\n }\n\n if (options.entityTypes?.length) {\n queryBuilder = queryBuilder.where('entity_type' as any, 'in', options.entityTypes)\n }\n\n const rows = await queryBuilder.execute() as Array<{ entity_type: string; entity_id: string; match_count: string | number }>\n\n return rows.map((row) => {\n const matchCount = typeof row.match_count === 'string'\n ? parseInt(row.match_count, 10)\n : row.match_count\n // Calculate score based on match ratio\n const score = matchCount / hashes.length\n\n return {\n entityId: row.entity_type as EntityId,\n recordId: row.entity_id,\n score,\n source: this.id,\n }\n })\n }\n\n async index(record: IndexableRecord): Promise<void> {\n // Dynamically import to avoid circular dependencies\n const { replaceSearchTokensForRecord } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n await replaceSearchTokensForRecord(this.db, {\n entityType: record.entityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n doc: record.fields,\n })\n }\n\n async delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void> {\n // Dynamically import to avoid circular dependencies\n const { deleteSearchTokensForRecord } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n await deleteSearchTokensForRecord(this.db, {\n entityType: entityId,\n recordId,\n tenantId,\n })\n }\n\n async bulkIndex(records: IndexableRecord[]): Promise<void> {\n if (records.length === 0) return\n\n const { replaceSearchTokensForBatch } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n const payloads = records.map((record) => ({\n entityType: record.entityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n doc: record.fields as Record<string, unknown>,\n }))\n\n await replaceSearchTokensForBatch(this.db, payloads)\n }\n\n async purge(entityId: EntityId, tenantId: string): Promise<void> {\n await this.db\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', entityId)\n .where('tenant_id' as any, '=', tenantId)\n .execute()\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { type Kysely, sql, type SqlBool } from 'kysely'\nimport type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n IndexableRecord,\n} from '../types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\n\n/**\n * Configuration for TokenSearchStrategy.\n */\nexport type TokenStrategyConfig = {\n /** Minimum number of query tokens that must match (0-1 ratio, default 0.5) */\n minMatchRatio?: number\n /** Default limit for search results */\n defaultLimit?: number\n}\n\nfunction normalizeOrganizationIds(options: SearchOptions): string[] | null {\n const single = typeof options.organizationId === 'string' ? options.organizationId.trim() : ''\n if (single) return [single]\n if (!Array.isArray(options.organizationIds)) return null\n return Array.from(new Set(\n options.organizationIds\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter((value) => value.length > 0),\n ))\n}\n\n/**\n * TokenSearchStrategy provides hash-based search using the existing search_tokens table.\n * This strategy is always available and serves as a fallback when other strategies fail.\n *\n * It tokenizes queries into hashes and matches against pre-indexed token hashes,\n * enabling search on encrypted fields without exposing plaintext to external services.\n */\nexport class TokenSearchStrategy implements SearchStrategy {\n readonly id: SearchStrategyId = 'tokens'\n readonly name = 'Token Search'\n readonly priority = 10 // Lowest priority, always available as fallback\n\n private readonly minMatchRatio: number\n private readonly defaultLimit: number\n\n constructor(\n private readonly db: Kysely<any>,\n config?: TokenStrategyConfig,\n ) {\n this.minMatchRatio = config?.minMatchRatio ?? 0.5\n this.defaultLimit = config?.defaultLimit ?? 50\n }\n\n async isAvailable(): Promise<boolean> {\n return true // Always available\n }\n\n async ensureReady(): Promise<void> {\n // No initialization needed\n }\n\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n const organizationIds = normalizeOrganizationIds(options)\n if (organizationIds && organizationIds.length === 0) return []\n\n // Dynamically import tokenization to avoid circular dependencies\n const { tokenizeText } = await import('@open-mercato/shared/lib/search/tokenize')\n const { resolveSearchConfig } = await import('@open-mercato/shared/lib/search/config')\n\n const config = resolveSearchConfig()\n if (!config.enabled) return []\n\n const { hashes } = tokenizeText(query, config)\n if (hashes.length === 0) return []\n\n const minMatches = Math.max(1, Math.ceil(hashes.length * this.minMatchRatio))\n const limit = options.limit ?? this.defaultLimit\n\n let queryBuilder = this.db\n .selectFrom('search_tokens' as any)\n .select([\n 'entity_type' as any,\n 'entity_id' as any,\n 'organization_id' as any,\n sql<string>`count(*)`.as('match_count'),\n ])\n .where('token_hash' as any, 'in', hashes)\n .where('tenant_id' as any, '=', options.tenantId)\n .groupBy(['entity_type' as any, 'entity_id' as any, 'organization_id' as any])\n .having(sql<SqlBool>`count(distinct token_hash) >= ${minMatches}`)\n .orderBy(sql`count(distinct token_hash) desc`)\n .limit(limit)\n\n if (organizationIds) {\n queryBuilder = queryBuilder.where('organization_id' as any, 'in', organizationIds)\n }\n\n if (options.entityTypes?.length) {\n queryBuilder = queryBuilder.where('entity_type' as any, 'in', options.entityTypes)\n }\n\n const rows = await queryBuilder.execute() as Array<{\n entity_type: string\n entity_id: string\n organization_id: string | null\n match_count: string | number\n }>\n\n return rows.map((row) => {\n const matchCount = typeof row.match_count === 'string'\n ? parseInt(row.match_count, 10)\n : row.match_count\n // Calculate score based on match ratio\n const score = matchCount / hashes.length\n\n return {\n entityId: row.entity_type as EntityId,\n recordId: row.entity_id,\n score,\n source: this.id,\n organizationId: row.organization_id ?? null,\n }\n })\n }\n\n async index(record: IndexableRecord): Promise<void> {\n // Dynamically import to avoid circular dependencies\n const { replaceSearchTokensForRecord } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n await replaceSearchTokensForRecord(this.db, {\n entityType: record.entityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n doc: record.fields,\n })\n }\n\n async delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void> {\n // Dynamically import to avoid circular dependencies\n const { deleteSearchTokensForRecord } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n await deleteSearchTokensForRecord(this.db, {\n entityType: entityId,\n recordId,\n tenantId,\n })\n }\n\n async bulkIndex(records: IndexableRecord[]): Promise<void> {\n if (records.length === 0) return\n\n const { replaceSearchTokensForBatch } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n const payloads = records.map((record) => ({\n entityType: record.entityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n doc: record.fields as Record<string, unknown>,\n }))\n\n await replaceSearchTokensForBatch(this.db, payloads)\n }\n\n async purge(entityId: EntityId, tenantId: string): Promise<void> {\n await this.db\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', entityId)\n .where('tenant_id' as any, '=', tenantId)\n .execute()\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,13 @@
1
1
  import { createHash } from "crypto";
2
2
  import { searchDebugWarn } from "../lib/debug.js";
3
+ function normalizeOrganizationIds(options) {
4
+ const single = typeof options.organizationId === "string" ? options.organizationId.trim() : "";
5
+ if (single) return [single];
6
+ if (!Array.isArray(options.organizationIds)) return null;
7
+ return Array.from(new Set(
8
+ options.organizationIds.map((value) => typeof value === "string" ? value.trim() : "").filter((value) => value.length > 0)
9
+ ));
10
+ }
3
11
  class VectorSearchStrategy {
4
12
  constructor(embeddingService, vectorDriver, config) {
5
13
  this.embeddingService = embeddingService;
@@ -24,14 +32,19 @@ class VectorSearchStrategy {
24
32
  return this.readyPromise;
25
33
  }
26
34
  async search(query, options) {
35
+ const organizationIds = normalizeOrganizationIds(options);
36
+ if (organizationIds && organizationIds.length === 0) return [];
27
37
  await this.ensureReady();
28
38
  const embedding = await this.embeddingService.createEmbedding(query);
29
39
  const filter = {
30
40
  tenantId: options.tenantId,
31
41
  entityIds: options.entityTypes
32
42
  };
33
- if (options.organizationId) {
34
- filter.organizationId = options.organizationId;
43
+ if (organizationIds) {
44
+ filter.organizationIds = organizationIds;
45
+ if (organizationIds.length === 1) {
46
+ filter.organizationId = organizationIds[0];
47
+ }
35
48
  }
36
49
  const results = await this.vectorDriver.query({
37
50
  vector: embedding,
@@ -43,6 +56,7 @@ class VectorSearchStrategy {
43
56
  recordId: hit.recordId,
44
57
  score: hit.score,
45
58
  source: this.id,
59
+ organizationId: hit.organizationId ?? null,
46
60
  presenter: hit.presenter ?? void 0,
47
61
  url: hit.primaryLinkHref ?? hit.url ?? void 0,
48
62
  links: hit.links?.map((link) => ({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/strategies/vector.strategy.ts"],
4
- "sourcesContent": ["import { createHash } from 'crypto'\nimport type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n IndexableRecord,\n} from '../types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { VectorDriver, VectorDriverDocument } from '../vector/types'\nimport { searchDebugWarn } from '../lib/debug'\n\n/**\n * Embedding service interface - minimal subset needed by VectorSearchStrategy.\n */\nexport interface EmbeddingService {\n createEmbedding(text: string): Promise<number[]>\n available: boolean\n}\n\n/**\n * Configuration for VectorSearchStrategy.\n */\nexport type VectorStrategyConfig = {\n /** Default limit for search results */\n defaultLimit?: number\n}\n\n/**\n * VectorSearchStrategy provides semantic search using embeddings.\n * It wraps the existing vector module infrastructure.\n */\nexport class VectorSearchStrategy implements SearchStrategy {\n readonly id: SearchStrategyId = 'vector'\n readonly name = 'Vector Search'\n readonly priority = 20 // Medium priority\n\n private readonly defaultLimit: number\n private ready = false\n private readyPromise: Promise<void> | null = null\n\n constructor(\n private readonly embeddingService: EmbeddingService,\n private readonly vectorDriver: VectorDriver,\n config?: VectorStrategyConfig,\n ) {\n this.defaultLimit = config?.defaultLimit ?? 20\n }\n\n async isAvailable(): Promise<boolean> {\n return this.embeddingService.available\n }\n\n async ensureReady(): Promise<void> {\n if (this.ready) return\n if (!this.readyPromise) {\n this.readyPromise = this.vectorDriver.ensureReady().then(() => {\n this.ready = true\n })\n }\n return this.readyPromise\n }\n\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n await this.ensureReady()\n const embedding = await this.embeddingService.createEmbedding(query)\n\n // Build filter - only include organizationId if it's a real value\n // The pgvector driver treats null as \"only records with null org_id\",\n // but we want null/undefined to mean \"no organization filter\"\n const filter: {\n tenantId: string\n organizationId?: string | null\n entityIds?: EntityId[]\n } = {\n tenantId: options.tenantId,\n entityIds: options.entityTypes as EntityId[],\n }\n\n // Only add organizationId filter if it's a real org ID\n if (options.organizationId) {\n filter.organizationId = options.organizationId\n }\n\n const results = await this.vectorDriver.query({\n vector: embedding,\n limit: options.limit ?? this.defaultLimit,\n filter,\n })\n\n return results.map((hit) => ({\n entityId: hit.entityId,\n recordId: hit.recordId,\n score: hit.score,\n source: this.id,\n presenter: hit.presenter ?? undefined,\n url: hit.primaryLinkHref ?? hit.url ?? undefined,\n links: hit.links?.map((link) => ({\n href: link.href,\n label: link.label ?? '',\n kind: link.kind,\n })),\n metadata: hit.payload ?? undefined,\n }))\n }\n\n async index(record: IndexableRecord): Promise<void> {\n await this.ensureReady()\n // Use text from buildSource if available, otherwise fall back to generic extraction\n const textContent = record.text\n ? (Array.isArray(record.text) ? record.text.join('\\n') : record.text)\n : this.buildTextContent(record)\n if (!textContent) return\n\n const embedding = await this.embeddingService.createEmbedding(textContent)\n\n const doc: VectorDriverDocument = {\n entityId: record.entityId as EntityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n checksum: this.computeChecksum(record),\n embedding,\n url: record.url,\n presenter: record.presenter,\n links: record.links,\n driverId: this.vectorDriver.id,\n resultTitle: record.presenter?.title ?? record.recordId,\n resultSubtitle: record.presenter?.subtitle,\n resultIcon: record.presenter?.icon,\n resultBadge: record.presenter?.badge,\n }\n\n await this.vectorDriver.upsert(doc)\n }\n\n async delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void> {\n await this.ensureReady()\n await this.vectorDriver.delete(entityId, recordId, tenantId)\n }\n\n async purge(entityId: EntityId, tenantId: string): Promise<void> {\n await this.ensureReady()\n if (this.vectorDriver.purge) {\n await this.vectorDriver.purge(entityId, tenantId)\n }\n }\n\n /**\n * Build text content from record fields for embedding.\n */\n private buildTextContent(record: IndexableRecord): string {\n const parts: string[] = []\n\n // Add presenter info\n if (record.presenter?.title) {\n parts.push(record.presenter.title)\n }\n if (record.presenter?.subtitle) {\n parts.push(record.presenter.subtitle)\n }\n\n // Add string fields from record\n for (const [, value] of Object.entries(record.fields)) {\n if (typeof value === 'string' && value.trim()) {\n parts.push(value)\n }\n }\n\n return parts.join(' ').trim()\n }\n\n /**\n * Compute a checksum for change detection using SHA-256.\n * Uses checksumSource from buildSource if available, otherwise uses fields/presenter/url.\n */\n private computeChecksum(record: IndexableRecord): string {\n const source = record.checksumSource !== undefined\n ? record.checksumSource\n : {\n fields: record.fields,\n presenter: record.presenter,\n url: record.url,\n }\n const content = JSON.stringify(source)\n return createHash('sha256').update(content).digest('hex').slice(0, 16)\n }\n\n /**\n * List entries in the vector index (for admin/debugging).\n */\n async listEntries(options: {\n tenantId: string\n organizationId?: string | null\n entityId?: string\n limit?: number\n offset?: number\n }): Promise<Array<{\n entityId: string\n recordId: string\n tenantId: string\n organizationId: string | null\n presenter?: unknown\n url?: string\n }>> {\n await this.ensureReady()\n // Delegate to vector driver's list method if available\n const listMethod = (this.vectorDriver as unknown as {\n list?: (options: {\n tenantId: string\n organizationId?: string | null\n entityId?: string\n limit?: number\n offset?: number\n }) => Promise<unknown[]>\n }).list\n\n if (typeof listMethod === 'function') {\n const entries = await listMethod.call(this.vectorDriver, options)\n return entries as Array<{\n entityId: string\n recordId: string\n tenantId: string\n organizationId: string | null\n presenter?: unknown\n url?: string\n }>\n }\n\n // Fallback: return empty array if driver doesn't support listing\n searchDebugWarn('VectorSearchStrategy', 'Vector driver does not support listing entries')\n return []\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createHash } from 'crypto'\nimport type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n IndexableRecord,\n} from '../types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { VectorDriver, VectorDriverDocument } from '../vector/types'\nimport { searchDebugWarn } from '../lib/debug'\n\n/**\n * Embedding service interface - minimal subset needed by VectorSearchStrategy.\n */\nexport interface EmbeddingService {\n createEmbedding(text: string): Promise<number[]>\n available: boolean\n}\n\n/**\n * Configuration for VectorSearchStrategy.\n */\nexport type VectorStrategyConfig = {\n /** Default limit for search results */\n defaultLimit?: number\n}\n\nfunction normalizeOrganizationIds(options: SearchOptions): string[] | null {\n const single = typeof options.organizationId === 'string' ? options.organizationId.trim() : ''\n if (single) return [single]\n if (!Array.isArray(options.organizationIds)) return null\n return Array.from(new Set(\n options.organizationIds\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter((value) => value.length > 0),\n ))\n}\n\n/**\n * VectorSearchStrategy provides semantic search using embeddings.\n * It wraps the existing vector module infrastructure.\n */\nexport class VectorSearchStrategy implements SearchStrategy {\n readonly id: SearchStrategyId = 'vector'\n readonly name = 'Vector Search'\n readonly priority = 20 // Medium priority\n\n private readonly defaultLimit: number\n private ready = false\n private readyPromise: Promise<void> | null = null\n\n constructor(\n private readonly embeddingService: EmbeddingService,\n private readonly vectorDriver: VectorDriver,\n config?: VectorStrategyConfig,\n ) {\n this.defaultLimit = config?.defaultLimit ?? 20\n }\n\n async isAvailable(): Promise<boolean> {\n return this.embeddingService.available\n }\n\n async ensureReady(): Promise<void> {\n if (this.ready) return\n if (!this.readyPromise) {\n this.readyPromise = this.vectorDriver.ensureReady().then(() => {\n this.ready = true\n })\n }\n return this.readyPromise\n }\n\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n const organizationIds = normalizeOrganizationIds(options)\n if (organizationIds && organizationIds.length === 0) return []\n\n await this.ensureReady()\n const embedding = await this.embeddingService.createEmbedding(query)\n\n // Build filter - only include organizationId if it's a real value\n // The pgvector driver treats null as \"only records with null org_id\",\n // but we want null/undefined to mean \"no organization filter\"\n const filter: {\n tenantId: string\n organizationId?: string | null\n organizationIds?: string[] | null\n entityIds?: EntityId[]\n } = {\n tenantId: options.tenantId,\n entityIds: options.entityTypes as EntityId[],\n }\n\n if (organizationIds) {\n filter.organizationIds = organizationIds\n if (organizationIds.length === 1) {\n filter.organizationId = organizationIds[0]\n }\n }\n\n const results = await this.vectorDriver.query({\n vector: embedding,\n limit: options.limit ?? this.defaultLimit,\n filter,\n })\n\n return results.map((hit) => ({\n entityId: hit.entityId,\n recordId: hit.recordId,\n score: hit.score,\n source: this.id,\n organizationId: hit.organizationId ?? null,\n presenter: hit.presenter ?? undefined,\n url: hit.primaryLinkHref ?? hit.url ?? undefined,\n links: hit.links?.map((link) => ({\n href: link.href,\n label: link.label ?? '',\n kind: link.kind,\n })),\n metadata: hit.payload ?? undefined,\n }))\n }\n\n async index(record: IndexableRecord): Promise<void> {\n await this.ensureReady()\n // Use text from buildSource if available, otherwise fall back to generic extraction\n const textContent = record.text\n ? (Array.isArray(record.text) ? record.text.join('\\n') : record.text)\n : this.buildTextContent(record)\n if (!textContent) return\n\n const embedding = await this.embeddingService.createEmbedding(textContent)\n\n const doc: VectorDriverDocument = {\n entityId: record.entityId as EntityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n checksum: this.computeChecksum(record),\n embedding,\n url: record.url,\n presenter: record.presenter,\n links: record.links,\n driverId: this.vectorDriver.id,\n resultTitle: record.presenter?.title ?? record.recordId,\n resultSubtitle: record.presenter?.subtitle,\n resultIcon: record.presenter?.icon,\n resultBadge: record.presenter?.badge,\n }\n\n await this.vectorDriver.upsert(doc)\n }\n\n async delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void> {\n await this.ensureReady()\n await this.vectorDriver.delete(entityId, recordId, tenantId)\n }\n\n async purge(entityId: EntityId, tenantId: string): Promise<void> {\n await this.ensureReady()\n if (this.vectorDriver.purge) {\n await this.vectorDriver.purge(entityId, tenantId)\n }\n }\n\n /**\n * Build text content from record fields for embedding.\n */\n private buildTextContent(record: IndexableRecord): string {\n const parts: string[] = []\n\n // Add presenter info\n if (record.presenter?.title) {\n parts.push(record.presenter.title)\n }\n if (record.presenter?.subtitle) {\n parts.push(record.presenter.subtitle)\n }\n\n // Add string fields from record\n for (const [, value] of Object.entries(record.fields)) {\n if (typeof value === 'string' && value.trim()) {\n parts.push(value)\n }\n }\n\n return parts.join(' ').trim()\n }\n\n /**\n * Compute a checksum for change detection using SHA-256.\n * Uses checksumSource from buildSource if available, otherwise uses fields/presenter/url.\n */\n private computeChecksum(record: IndexableRecord): string {\n const source = record.checksumSource !== undefined\n ? record.checksumSource\n : {\n fields: record.fields,\n presenter: record.presenter,\n url: record.url,\n }\n const content = JSON.stringify(source)\n return createHash('sha256').update(content).digest('hex').slice(0, 16)\n }\n\n /**\n * List entries in the vector index (for admin/debugging).\n */\n async listEntries(options: {\n tenantId: string\n organizationId?: string | null\n entityId?: string\n limit?: number\n offset?: number\n }): Promise<Array<{\n entityId: string\n recordId: string\n tenantId: string\n organizationId: string | null\n presenter?: unknown\n url?: string\n }>> {\n await this.ensureReady()\n // Delegate to vector driver's list method if available\n const listMethod = (this.vectorDriver as unknown as {\n list?: (options: {\n tenantId: string\n organizationId?: string | null\n entityId?: string\n limit?: number\n offset?: number\n }) => Promise<unknown[]>\n }).list\n\n if (typeof listMethod === 'function') {\n const entries = await listMethod.call(this.vectorDriver, options)\n return entries as Array<{\n entityId: string\n recordId: string\n tenantId: string\n organizationId: string | null\n presenter?: unknown\n url?: string\n }>\n }\n\n // Fallback: return empty array if driver doesn't support listing\n searchDebugWarn('VectorSearchStrategy', 'Vector driver does not support listing entries')\n return []\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -252,11 +252,17 @@ function createPgVectorDriver(opts = {}) {
252
252
  const vectorLiteral = toVectorLiteral(input.vector);
253
253
  const filter = input.filter ?? { tenantId: "" };
254
254
  const normalizedOrganizationId = typeof filter.organizationId === "string" && filter.organizationId.trim().length > 0 ? filter.organizationId.trim() : null;
255
+ const normalizedOrganizationIds = normalizedOrganizationId ? [normalizedOrganizationId] : Array.isArray(filter.organizationIds) ? Array.from(new Set(
256
+ filter.organizationIds.map((value) => typeof value === "string" ? value.trim() : "").filter((value) => value.length > 0)
257
+ )) : null;
258
+ if (normalizedOrganizationIds && normalizedOrganizationIds.length === 0) {
259
+ return [];
260
+ }
255
261
  const params = [
256
262
  vectorLiteral,
257
263
  DRIVER_ID,
258
264
  filter.tenantId,
259
- normalizedOrganizationId,
265
+ normalizedOrganizationIds,
260
266
  Array.isArray(filter.entityIds) && filter.entityIds.length ? filter.entityIds : null,
261
267
  input.limit ?? 20
262
268
  ];
@@ -282,7 +288,7 @@ function createPgVectorDriver(opts = {}) {
282
288
  FROM ${tableIdent}
283
289
  WHERE driver_id = $2
284
290
  AND tenant_id = $3::uuid
285
- AND ($4::uuid IS NULL OR organization_id = $4::uuid)
291
+ AND ($4::uuid[] IS NULL OR organization_id = ANY($4::uuid[]))
286
292
  AND (
287
293
  $5::text[] IS NULL OR entity_id = ANY($5::text[])
288
294
  )
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/vector/drivers/pgvector/index.ts"],
4
- "sourcesContent": ["import { Pool } from 'pg'\nimport { searchDebugWarn } from '../../../lib/debug'\n\ntype PgPoolQueryResult<T> = { rows: T[]; rowCount?: number }\ntype PgPoolClient = {\n query<T = any>(text: string, params?: any[]): Promise<PgPoolQueryResult<T>>\n release(): void\n}\ntype PgPool = {\n connect(): Promise<PgPoolClient>\n query<T = any>(text: string, params?: any[]): Promise<PgPoolQueryResult<T>>\n end(): Promise<void>\n}\nimport type {\n VectorDriver,\n VectorDriverDocument,\n VectorDriverQuery,\n VectorDriverQueryResult,\n VectorDriverListParams,\n VectorDriverCountParams,\n VectorIndexEntry,\n VectorDriverRemoveOrphansParams,\n VectorResultPresenter,\n VectorLinkDescriptor,\n} from '../../types'\n\ntype PgVectorDriverOptions = {\n pool?: PgPool\n connectionString?: string\n tableName?: string\n migrationsTable?: string\n dimension?: number\n distanceMetric?: 'cosine' | 'euclidean' | 'inner'\n}\n\nconst DEFAULT_TABLE = 'vector_search'\nconst DEFAULT_MIGRATIONS_TABLE = 'vector_search_migrations'\nconst DEFAULT_DIMENSION = 1536\nconst DRIVER_ID = 'pgvector' as const\n\nfunction assertIdentifier(name: string, defaultName: string): string {\n const candidate = name ?? defaultName\n if (!candidate || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(candidate)) return defaultName\n return candidate\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name}\"`\n}\n\nfunction toVectorLiteral(values: number[]): string {\n const formatted = values.map((n) => {\n if (!Number.isFinite(n)) return '0'\n const rounded = Math.fround(n)\n return Number.isInteger(rounded) ? `${rounded}.0` : `${rounded}`\n })\n return `[${formatted.join(',')}]`\n}\n\nfunction parseJsonColumn<T>(value: unknown): T | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') {\n try {\n return JSON.parse(value) as T\n } catch {\n // When `jsonb` stores a JSON string, node-postgres parses it into a plain JS string.\n // In that case, there is nothing to JSON.parse \u2014 return the raw string value.\n return value as unknown as T\n }\n }\n if (typeof value === 'object') {\n return value as T\n }\n return null\n}\n\nasync function withClient<T>(pool: PgPool, fn: (client: PgPoolClient) => Promise<T>): Promise<T> {\n const client = await pool.connect()\n try {\n return await fn(client)\n } finally {\n client.release()\n }\n}\n\nexport function createPgVectorDriver(opts: PgVectorDriverOptions = {}): VectorDriver {\n const tableName = assertIdentifier(opts.tableName ?? DEFAULT_TABLE, DEFAULT_TABLE)\n const migrationsTable = assertIdentifier(opts.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE, DEFAULT_MIGRATIONS_TABLE)\n let dimension = opts.dimension ?? DEFAULT_DIMENSION\n const distanceMetric = opts.distanceMetric ?? 'cosine'\n const tableIdent = quoteIdent(tableName)\n const migrationsIdent = quoteIdent(migrationsTable)\n\n const pool: PgPool =\n opts.pool ??\n (() => {\n const conn = opts.connectionString ?? process.env.DATABASE_URL\n if (!conn) {\n throw new Error('[vector.pgvector] DATABASE_URL is not configured')\n }\n return new Pool({ connectionString: conn }) as unknown as PgPool\n })()\n\n let ready: Promise<void> | null = null\n\n const ensureReady = async () => {\n if (!ready) {\n ready = withClient(pool, async (client) => {\n const ensureExtension = async (extension: 'pgcrypto' | 'vector') => {\n try {\n await client.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`)\n } catch (error) {\n const pgError = error as { code?: string; message?: string }\n if (pgError?.code === '42501') {\n const details = pgError.message ? ` (${pgError.message})` : ''\n searchDebugWarn('vector.pgvector', `skipping ${extension} extension creation; requires superuser${details}`)\n return\n }\n throw error\n }\n }\n\n await ensureExtension('pgcrypto')\n await ensureExtension('vector')\n\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${migrationsIdent} (\n id text primary key,\n applied_at timestamptz not null default now()\n )`,\n )\n\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${tableIdent} (\n id uuid primary key default gen_random_uuid(),\n driver_id text not null,\n entity_id text not null,\n record_id text not null,\n tenant_id uuid not null,\n organization_id uuid null,\n checksum text not null,\n embedding vector(${dimension}) not null,\n url text null,\n presenter jsonb null,\n links jsonb null,\n payload jsonb null,\n result_title text null,\n result_subtitle text null,\n result_icon text null,\n result_badge text null,\n result_snapshot text null,\n primary_link_href text null,\n primary_link_label text null,\n created_at timestamptz not null default now(),\n updated_at timestamptz not null default now()\n )`,\n )\n\n await client.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS ${tableName}_uniq ON ${tableIdent} (driver_id, entity_id, record_id, tenant_id)`,\n )\n await client.query(\n `CREATE INDEX IF NOT EXISTS ${tableName}_lookup ON ${tableIdent} (tenant_id, organization_id, entity_id)`,\n )\n // ivfflat index only supports up to 2000 dimensions\n // For higher dimensions, skip the index (uses sequential scan, slower but works)\n // Also check actual table dimension in case driver was initialized with different value\n let actualDimension = dimension\n try {\n const dimResult = await client.query<{ atttypmod: number }>(\n `SELECT a.atttypmod\n FROM pg_attribute a\n JOIN pg_class c ON a.attrelid = c.oid\n WHERE c.relname = $1\n AND a.attname = 'embedding'\n AND a.atttypmod > 0`,\n [tableName]\n )\n if (dimResult.rows.length > 0 && dimResult.rows[0].atttypmod > 0) {\n actualDimension = dimResult.rows[0].atttypmod\n }\n } catch {\n // Ignore errors reading dimension, use configured value\n }\n\n if (actualDimension <= 2000) {\n try {\n await client.query(\n `CREATE INDEX IF NOT EXISTS ${tableName}_embedding_idx ON ${tableIdent}\n USING ivfflat (embedding vector_${distanceMetric}_ops) WITH (lists = 100)`,\n )\n } catch (indexErr: unknown) {\n // Handle case where dimension exceeds ivfflat limit\n const errorMessage = indexErr instanceof Error ? indexErr.message : String(indexErr)\n if (errorMessage.includes('2000 dimensions')) {\n searchDebugWarn('pgvector', 'Skipping ivfflat index - dimension exceeds 2000 limit. Searches will use sequential scan.')\n } else {\n throw indexErr\n }\n }\n } else {\n searchDebugWarn('pgvector', `Skipping ivfflat index - dimension ${actualDimension} exceeds 2000 limit. Searches will use sequential scan.`)\n }\n\n const columnAlters = [\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_title text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_subtitle text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_icon text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_badge text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_snapshot text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS primary_link_href text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS primary_link_label text`,\n ]\n for (const statement of columnAlters) {\n await client.query(statement)\n }\n\n await client.query(\n `INSERT INTO ${migrationsIdent} (id, applied_at) VALUES ($1, now()) ON CONFLICT (id) DO NOTHING`,\n ['0001_init'],\n )\n }).catch((err) => {\n ready = null\n throw err\n })\n }\n return ready\n }\n\n const upsert = async (doc: VectorDriverDocument) => {\n await ensureReady()\n const vectorLiteral = toVectorLiteral(doc.embedding)\n await pool.query(\n `\n INSERT INTO ${tableIdent} (\n driver_id, entity_id, record_id, tenant_id, organization_id, checksum,\n embedding, url, presenter, links, payload,\n result_title, result_subtitle, result_icon, result_badge, result_snapshot,\n primary_link_href, primary_link_label,\n created_at, updated_at\n )\n VALUES (\n $1, $2, $3, $4::uuid, $5::uuid, $6, $7::vector, $8, $9::jsonb, $10::jsonb, $11::jsonb,\n $12, $13, $14, $15, $16, $17, $18,\n now(), now()\n )\n ON CONFLICT (driver_id, entity_id, record_id, tenant_id)\n DO UPDATE SET\n organization_id = EXCLUDED.organization_id,\n checksum = EXCLUDED.checksum,\n embedding = EXCLUDED.embedding,\n url = EXCLUDED.url,\n presenter = EXCLUDED.presenter,\n links = EXCLUDED.links,\n payload = EXCLUDED.payload,\n result_title = EXCLUDED.result_title,\n result_subtitle = EXCLUDED.result_subtitle,\n result_icon = EXCLUDED.result_icon,\n result_badge = EXCLUDED.result_badge,\n result_snapshot = EXCLUDED.result_snapshot,\n primary_link_href = EXCLUDED.primary_link_href,\n primary_link_label = EXCLUDED.primary_link_label,\n updated_at = now()\n `,\n [\n doc.driverId ?? DRIVER_ID,\n doc.entityId,\n doc.recordId,\n doc.tenantId,\n doc.organizationId ?? null,\n doc.checksum,\n vectorLiteral,\n doc.url ?? null,\n doc.presenter ? JSON.stringify(doc.presenter) : null,\n doc.links ? JSON.stringify(doc.links) : null,\n doc.payload ? JSON.stringify(doc.payload) : null,\n doc.resultTitle,\n doc.resultSubtitle ?? null,\n doc.resultIcon ?? null,\n doc.resultBadge ?? null,\n doc.resultSnapshot ?? null,\n doc.primaryLinkHref ?? null,\n doc.primaryLinkLabel ?? null,\n ],\n )\n }\n\n const remove = async (entityId: string, recordId: string, tenantId: string) => {\n await ensureReady()\n await pool.query(\n `DELETE FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND record_id = $3 AND tenant_id = $4::uuid`,\n [DRIVER_ID, entityId, recordId, tenantId],\n )\n }\n\n const getChecksum = async (entityId: string, recordId: string, tenantId: string): Promise<string | null> => {\n await ensureReady()\n const res = await pool.query<{ checksum: string }>(\n `SELECT checksum FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND record_id = $3 AND tenant_id = $4::uuid`,\n [DRIVER_ID, entityId, recordId, tenantId],\n )\n return res.rowCount ? res.rows[0].checksum : null\n }\n\n const purge = async (entityId: string, tenantId: string) => {\n await ensureReady()\n await pool.query(\n `DELETE FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND tenant_id = $3::uuid`,\n [DRIVER_ID, entityId, tenantId],\n )\n }\n\n const query = async (input: VectorDriverQuery): Promise<VectorDriverQueryResult[]> => {\n await ensureReady()\n const vectorLiteral = toVectorLiteral(input.vector)\n const filter = input.filter ?? { tenantId: '' }\n const normalizedOrganizationId =\n typeof filter.organizationId === 'string' && filter.organizationId.trim().length > 0\n ? filter.organizationId.trim()\n : null\n const params: any[] = [\n vectorLiteral,\n DRIVER_ID,\n filter.tenantId,\n normalizedOrganizationId,\n Array.isArray(filter.entityIds) && filter.entityIds.length ? filter.entityIds : null,\n input.limit ?? 20,\n ]\n const res = await pool.query<{\n entity_id: string\n record_id: string\n organization_id: string | null\n checksum: string\n url: string | null\n presenter: string | null\n links: string | null\n payload: string | null\n result_title: string | null\n result_subtitle: string | null\n result_icon: string | null\n result_badge: string | null\n result_snapshot: string | null\n primary_link_href: string | null\n primary_link_label: string | null\n distance: number\n }>(\n `\n SELECT\n entity_id,\n record_id,\n organization_id,\n checksum,\n url,\n presenter,\n links,\n payload,\n result_title,\n result_subtitle,\n result_icon,\n result_badge,\n result_snapshot,\n primary_link_href,\n primary_link_label,\n embedding <=> $1::vector AS distance\n FROM ${tableIdent}\n WHERE driver_id = $2\n AND tenant_id = $3::uuid\n AND ($4::uuid IS NULL OR organization_id = $4::uuid)\n AND (\n $5::text[] IS NULL OR entity_id = ANY($5::text[])\n )\n ORDER BY embedding <=> $1::vector\n LIMIT $6\n `,\n params,\n )\n return res.rows.map<VectorDriverQueryResult>((row) => {\n const distance = typeof row.distance === 'number' ? row.distance : Number(row.distance || 1)\n const score = 1 - distance\n return {\n entityId: row.entity_id,\n recordId: row.record_id,\n organizationId: row.organization_id ?? null,\n checksum: row.checksum,\n url: row.url ?? null,\n presenter: parseJsonColumn<VectorResultPresenter>(row.presenter),\n links: parseJsonColumn<VectorLinkDescriptor[]>(row.links),\n payload: parseJsonColumn<Record<string, unknown>>(row.payload),\n resultTitle: row.result_title ?? '',\n resultSubtitle: row.result_subtitle ?? null,\n resultIcon: row.result_icon ?? null,\n resultBadge: row.result_badge ?? null,\n resultSnapshot: row.result_snapshot ?? null,\n primaryLinkHref: row.primary_link_href ?? null,\n primaryLinkLabel: row.primary_link_label ?? null,\n score,\n }\n })\n }\n\n const list = async (params: VectorDriverListParams): Promise<VectorIndexEntry[]> => {\n await ensureReady()\n const limit = Math.max(1, Math.min(params.limit ?? 50, 200))\n const offset = Math.max(0, params.offset ?? 0)\n const orderColumn = params.orderBy === 'created' ? 'created_at' : 'updated_at'\n const conditions: string[] = [\n 'driver_id = $1',\n 'tenant_id = $2::uuid',\n ]\n const values: any[] = [DRIVER_ID, params.tenantId]\n let nextParam = 3\n\n const normalizedOrganizationId =\n typeof params.organizationId === 'string' && params.organizationId.trim().length > 0\n ? params.organizationId.trim()\n : null\n if (normalizedOrganizationId !== null) {\n conditions.push(`organization_id = $${nextParam}::uuid`)\n values.push(normalizedOrganizationId)\n nextParam += 1\n }\n\n if (params.entityId) {\n conditions.push(`entity_id = $${nextParam}::text`)\n values.push(params.entityId)\n nextParam += 1\n }\n\n const limitParam = nextParam\n const offsetParam = nextParam + 1\n values.push(limit, offset)\n\n const sql = `\n SELECT\n entity_id,\n record_id,\n tenant_id,\n organization_id,\n checksum,\n url,\n presenter,\n links,\n payload,\n result_title,\n result_subtitle,\n result_icon,\n result_badge,\n result_snapshot,\n primary_link_href,\n primary_link_label,\n created_at,\n updated_at\n FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n ORDER BY ${orderColumn} DESC\n LIMIT $${limitParam} OFFSET $${offsetParam}\n `\n\n const res = await pool.query<{\n entity_id: string\n record_id: string\n tenant_id: string\n organization_id: string | null\n checksum: string\n url: string | null\n presenter: string | null\n links: string | null\n payload: string | null\n result_title: string | null\n result_subtitle: string | null\n result_icon: string | null\n result_badge: string | null\n result_snapshot: string | null\n primary_link_href: string | null\n primary_link_label: string | null\n created_at: Date | string\n updated_at: Date | string\n }>(sql, values)\n return res.rows.map<VectorIndexEntry>((row) => {\n const presenter = parseJsonColumn<VectorResultPresenter>(row.presenter)\n const links = parseJsonColumn<VectorLinkDescriptor[]>(row.links)\n const payload = parseJsonColumn<Record<string, unknown>>(row.payload)\n const createdAt =\n row.created_at instanceof Date\n ? row.created_at.toISOString()\n : new Date(row.created_at ?? Date.now()).toISOString()\n const updatedAt =\n row.updated_at instanceof Date\n ? row.updated_at.toISOString()\n : new Date(row.updated_at ?? Date.now()).toISOString()\n return {\n entityId: row.entity_id,\n recordId: row.record_id,\n driverId: DRIVER_ID,\n tenantId: row.tenant_id,\n organizationId: row.organization_id ?? null,\n checksum: row.checksum,\n url: row.url ?? null,\n presenter,\n links,\n payload,\n metadata: payload,\n resultTitle: row.result_title ?? '',\n resultSubtitle: row.result_subtitle ?? null,\n resultIcon: row.result_icon ?? null,\n resultBadge: row.result_badge ?? null,\n resultSnapshot: row.result_snapshot ?? null,\n primaryLinkHref: row.primary_link_href ?? null,\n primaryLinkLabel: row.primary_link_label ?? null,\n createdAt,\n updatedAt,\n score: null,\n }\n })\n }\n\n const count = async (params: VectorDriverCountParams): Promise<number> => {\n await ensureReady()\n const conditions: string[] = [\n 'driver_id = $1',\n 'tenant_id = $2::uuid',\n ]\n const values: any[] = [DRIVER_ID, params.tenantId]\n let nextParam = 3\n\n const normalizedOrganizationId =\n typeof params.organizationId === 'string' && params.organizationId.trim().length > 0\n ? params.organizationId.trim()\n : null\n if (normalizedOrganizationId !== null) {\n conditions.push(`organization_id = $${nextParam}::uuid`)\n values.push(normalizedOrganizationId)\n nextParam += 1\n }\n if (params.entityId) {\n conditions.push(`entity_id = $${nextParam}::text`)\n values.push(params.entityId)\n nextParam += 1\n }\n\n const sql = `\n SELECT count(*)::bigint AS total\n FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n `\n const res = await pool.query<{ total: string }>(sql, values)\n const raw = res.rows?.[0]?.total\n if (!raw) return 0\n const parsed = Number(raw)\n return Number.isFinite(parsed) ? parsed : 0\n }\n\n const removeOrphans = async (params: VectorDriverRemoveOrphansParams): Promise<number> => {\n await ensureReady()\n const conditions: string[] = [\n 'driver_id = $1',\n 'entity_id = $2',\n 'updated_at < $3::timestamptz',\n ]\n const values: any[] = [DRIVER_ID, params.entityId, (params.olderThan instanceof Date ? params.olderThan : new Date(params.olderThan)).toISOString()]\n let nextParam = 4\n\n if (params.tenantId !== undefined) {\n conditions.push(`tenant_id is not distinct from $${nextParam}::uuid`)\n values.push(params.tenantId)\n nextParam += 1\n }\n\n if (params.organizationId !== undefined) {\n conditions.push(`organization_id is not distinct from $${nextParam}::uuid`)\n values.push(params.organizationId)\n nextParam += 1\n }\n\n const sql = `\n DELETE FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n `\n const res = await pool.query(sql, values)\n return res.rowCount ?? 0\n }\n\n const getTableDimension = async (): Promise<number | null> => {\n try {\n const res = await pool.query<{ atttypmod: number }>(\n `SELECT a.atttypmod\n FROM pg_attribute a\n JOIN pg_class c ON a.attrelid = c.oid\n WHERE c.relname = $1\n AND a.attname = 'embedding'\n AND a.atttypmod > 0`,\n [tableName]\n )\n if (res.rows.length > 0 && res.rows[0].atttypmod > 0) {\n return res.rows[0].atttypmod\n }\n return null\n } catch {\n return null\n }\n }\n\n const recreateWithDimension = async (newDimension: number): Promise<void> => {\n await withClient(pool, async (client) => {\n await client.query(`DROP TABLE IF EXISTS ${tableIdent} CASCADE`)\n await client.query(`DROP TABLE IF EXISTS ${migrationsIdent} CASCADE`)\n })\n ready = null\n dimension = newDimension\n await ensureReady()\n }\n\n return {\n id: 'pgvector',\n ensureReady,\n upsert,\n delete: remove,\n getChecksum,\n purge,\n query,\n list,\n count,\n removeOrphans,\n getTableDimension,\n recreateWithDimension,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { Pool } from 'pg'\nimport { searchDebugWarn } from '../../../lib/debug'\n\ntype PgPoolQueryResult<T> = { rows: T[]; rowCount?: number }\ntype PgPoolClient = {\n query<T = any>(text: string, params?: any[]): Promise<PgPoolQueryResult<T>>\n release(): void\n}\ntype PgPool = {\n connect(): Promise<PgPoolClient>\n query<T = any>(text: string, params?: any[]): Promise<PgPoolQueryResult<T>>\n end(): Promise<void>\n}\nimport type {\n VectorDriver,\n VectorDriverDocument,\n VectorDriverQuery,\n VectorDriverQueryResult,\n VectorDriverListParams,\n VectorDriverCountParams,\n VectorIndexEntry,\n VectorDriverRemoveOrphansParams,\n VectorResultPresenter,\n VectorLinkDescriptor,\n} from '../../types'\n\ntype PgVectorDriverOptions = {\n pool?: PgPool\n connectionString?: string\n tableName?: string\n migrationsTable?: string\n dimension?: number\n distanceMetric?: 'cosine' | 'euclidean' | 'inner'\n}\n\nconst DEFAULT_TABLE = 'vector_search'\nconst DEFAULT_MIGRATIONS_TABLE = 'vector_search_migrations'\nconst DEFAULT_DIMENSION = 1536\nconst DRIVER_ID = 'pgvector' as const\n\nfunction assertIdentifier(name: string, defaultName: string): string {\n const candidate = name ?? defaultName\n if (!candidate || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(candidate)) return defaultName\n return candidate\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name}\"`\n}\n\nfunction toVectorLiteral(values: number[]): string {\n const formatted = values.map((n) => {\n if (!Number.isFinite(n)) return '0'\n const rounded = Math.fround(n)\n return Number.isInteger(rounded) ? `${rounded}.0` : `${rounded}`\n })\n return `[${formatted.join(',')}]`\n}\n\nfunction parseJsonColumn<T>(value: unknown): T | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') {\n try {\n return JSON.parse(value) as T\n } catch {\n // When `jsonb` stores a JSON string, node-postgres parses it into a plain JS string.\n // In that case, there is nothing to JSON.parse \u2014 return the raw string value.\n return value as unknown as T\n }\n }\n if (typeof value === 'object') {\n return value as T\n }\n return null\n}\n\nasync function withClient<T>(pool: PgPool, fn: (client: PgPoolClient) => Promise<T>): Promise<T> {\n const client = await pool.connect()\n try {\n return await fn(client)\n } finally {\n client.release()\n }\n}\n\nexport function createPgVectorDriver(opts: PgVectorDriverOptions = {}): VectorDriver {\n const tableName = assertIdentifier(opts.tableName ?? DEFAULT_TABLE, DEFAULT_TABLE)\n const migrationsTable = assertIdentifier(opts.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE, DEFAULT_MIGRATIONS_TABLE)\n let dimension = opts.dimension ?? DEFAULT_DIMENSION\n const distanceMetric = opts.distanceMetric ?? 'cosine'\n const tableIdent = quoteIdent(tableName)\n const migrationsIdent = quoteIdent(migrationsTable)\n\n const pool: PgPool =\n opts.pool ??\n (() => {\n const conn = opts.connectionString ?? process.env.DATABASE_URL\n if (!conn) {\n throw new Error('[vector.pgvector] DATABASE_URL is not configured')\n }\n return new Pool({ connectionString: conn }) as unknown as PgPool\n })()\n\n let ready: Promise<void> | null = null\n\n const ensureReady = async () => {\n if (!ready) {\n ready = withClient(pool, async (client) => {\n const ensureExtension = async (extension: 'pgcrypto' | 'vector') => {\n try {\n await client.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`)\n } catch (error) {\n const pgError = error as { code?: string; message?: string }\n if (pgError?.code === '42501') {\n const details = pgError.message ? ` (${pgError.message})` : ''\n searchDebugWarn('vector.pgvector', `skipping ${extension} extension creation; requires superuser${details}`)\n return\n }\n throw error\n }\n }\n\n await ensureExtension('pgcrypto')\n await ensureExtension('vector')\n\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${migrationsIdent} (\n id text primary key,\n applied_at timestamptz not null default now()\n )`,\n )\n\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${tableIdent} (\n id uuid primary key default gen_random_uuid(),\n driver_id text not null,\n entity_id text not null,\n record_id text not null,\n tenant_id uuid not null,\n organization_id uuid null,\n checksum text not null,\n embedding vector(${dimension}) not null,\n url text null,\n presenter jsonb null,\n links jsonb null,\n payload jsonb null,\n result_title text null,\n result_subtitle text null,\n result_icon text null,\n result_badge text null,\n result_snapshot text null,\n primary_link_href text null,\n primary_link_label text null,\n created_at timestamptz not null default now(),\n updated_at timestamptz not null default now()\n )`,\n )\n\n await client.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS ${tableName}_uniq ON ${tableIdent} (driver_id, entity_id, record_id, tenant_id)`,\n )\n await client.query(\n `CREATE INDEX IF NOT EXISTS ${tableName}_lookup ON ${tableIdent} (tenant_id, organization_id, entity_id)`,\n )\n // ivfflat index only supports up to 2000 dimensions\n // For higher dimensions, skip the index (uses sequential scan, slower but works)\n // Also check actual table dimension in case driver was initialized with different value\n let actualDimension = dimension\n try {\n const dimResult = await client.query<{ atttypmod: number }>(\n `SELECT a.atttypmod\n FROM pg_attribute a\n JOIN pg_class c ON a.attrelid = c.oid\n WHERE c.relname = $1\n AND a.attname = 'embedding'\n AND a.atttypmod > 0`,\n [tableName]\n )\n if (dimResult.rows.length > 0 && dimResult.rows[0].atttypmod > 0) {\n actualDimension = dimResult.rows[0].atttypmod\n }\n } catch {\n // Ignore errors reading dimension, use configured value\n }\n\n if (actualDimension <= 2000) {\n try {\n await client.query(\n `CREATE INDEX IF NOT EXISTS ${tableName}_embedding_idx ON ${tableIdent}\n USING ivfflat (embedding vector_${distanceMetric}_ops) WITH (lists = 100)`,\n )\n } catch (indexErr: unknown) {\n // Handle case where dimension exceeds ivfflat limit\n const errorMessage = indexErr instanceof Error ? indexErr.message : String(indexErr)\n if (errorMessage.includes('2000 dimensions')) {\n searchDebugWarn('pgvector', 'Skipping ivfflat index - dimension exceeds 2000 limit. Searches will use sequential scan.')\n } else {\n throw indexErr\n }\n }\n } else {\n searchDebugWarn('pgvector', `Skipping ivfflat index - dimension ${actualDimension} exceeds 2000 limit. Searches will use sequential scan.`)\n }\n\n const columnAlters = [\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_title text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_subtitle text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_icon text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_badge text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS result_snapshot text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS primary_link_href text`,\n `ALTER TABLE ${tableIdent} ADD COLUMN IF NOT EXISTS primary_link_label text`,\n ]\n for (const statement of columnAlters) {\n await client.query(statement)\n }\n\n await client.query(\n `INSERT INTO ${migrationsIdent} (id, applied_at) VALUES ($1, now()) ON CONFLICT (id) DO NOTHING`,\n ['0001_init'],\n )\n }).catch((err) => {\n ready = null\n throw err\n })\n }\n return ready\n }\n\n const upsert = async (doc: VectorDriverDocument) => {\n await ensureReady()\n const vectorLiteral = toVectorLiteral(doc.embedding)\n await pool.query(\n `\n INSERT INTO ${tableIdent} (\n driver_id, entity_id, record_id, tenant_id, organization_id, checksum,\n embedding, url, presenter, links, payload,\n result_title, result_subtitle, result_icon, result_badge, result_snapshot,\n primary_link_href, primary_link_label,\n created_at, updated_at\n )\n VALUES (\n $1, $2, $3, $4::uuid, $5::uuid, $6, $7::vector, $8, $9::jsonb, $10::jsonb, $11::jsonb,\n $12, $13, $14, $15, $16, $17, $18,\n now(), now()\n )\n ON CONFLICT (driver_id, entity_id, record_id, tenant_id)\n DO UPDATE SET\n organization_id = EXCLUDED.organization_id,\n checksum = EXCLUDED.checksum,\n embedding = EXCLUDED.embedding,\n url = EXCLUDED.url,\n presenter = EXCLUDED.presenter,\n links = EXCLUDED.links,\n payload = EXCLUDED.payload,\n result_title = EXCLUDED.result_title,\n result_subtitle = EXCLUDED.result_subtitle,\n result_icon = EXCLUDED.result_icon,\n result_badge = EXCLUDED.result_badge,\n result_snapshot = EXCLUDED.result_snapshot,\n primary_link_href = EXCLUDED.primary_link_href,\n primary_link_label = EXCLUDED.primary_link_label,\n updated_at = now()\n `,\n [\n doc.driverId ?? DRIVER_ID,\n doc.entityId,\n doc.recordId,\n doc.tenantId,\n doc.organizationId ?? null,\n doc.checksum,\n vectorLiteral,\n doc.url ?? null,\n doc.presenter ? JSON.stringify(doc.presenter) : null,\n doc.links ? JSON.stringify(doc.links) : null,\n doc.payload ? JSON.stringify(doc.payload) : null,\n doc.resultTitle,\n doc.resultSubtitle ?? null,\n doc.resultIcon ?? null,\n doc.resultBadge ?? null,\n doc.resultSnapshot ?? null,\n doc.primaryLinkHref ?? null,\n doc.primaryLinkLabel ?? null,\n ],\n )\n }\n\n const remove = async (entityId: string, recordId: string, tenantId: string) => {\n await ensureReady()\n await pool.query(\n `DELETE FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND record_id = $3 AND tenant_id = $4::uuid`,\n [DRIVER_ID, entityId, recordId, tenantId],\n )\n }\n\n const getChecksum = async (entityId: string, recordId: string, tenantId: string): Promise<string | null> => {\n await ensureReady()\n const res = await pool.query<{ checksum: string }>(\n `SELECT checksum FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND record_id = $3 AND tenant_id = $4::uuid`,\n [DRIVER_ID, entityId, recordId, tenantId],\n )\n return res.rowCount ? res.rows[0].checksum : null\n }\n\n const purge = async (entityId: string, tenantId: string) => {\n await ensureReady()\n await pool.query(\n `DELETE FROM ${tableIdent} WHERE driver_id = $1 AND entity_id = $2 AND tenant_id = $3::uuid`,\n [DRIVER_ID, entityId, tenantId],\n )\n }\n\n const query = async (input: VectorDriverQuery): Promise<VectorDriverQueryResult[]> => {\n await ensureReady()\n const vectorLiteral = toVectorLiteral(input.vector)\n const filter = input.filter ?? { tenantId: '' }\n const normalizedOrganizationId =\n typeof filter.organizationId === 'string' && filter.organizationId.trim().length > 0\n ? filter.organizationId.trim()\n : null\n const normalizedOrganizationIds = normalizedOrganizationId\n ? [normalizedOrganizationId]\n : Array.isArray(filter.organizationIds)\n ? Array.from(new Set(\n filter.organizationIds\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter((value) => value.length > 0),\n ))\n : null\n if (normalizedOrganizationIds && normalizedOrganizationIds.length === 0) {\n return []\n }\n const params: any[] = [\n vectorLiteral,\n DRIVER_ID,\n filter.tenantId,\n normalizedOrganizationIds,\n Array.isArray(filter.entityIds) && filter.entityIds.length ? filter.entityIds : null,\n input.limit ?? 20,\n ]\n const res = await pool.query<{\n entity_id: string\n record_id: string\n organization_id: string | null\n checksum: string\n url: string | null\n presenter: string | null\n links: string | null\n payload: string | null\n result_title: string | null\n result_subtitle: string | null\n result_icon: string | null\n result_badge: string | null\n result_snapshot: string | null\n primary_link_href: string | null\n primary_link_label: string | null\n distance: number\n }>(\n `\n SELECT\n entity_id,\n record_id,\n organization_id,\n checksum,\n url,\n presenter,\n links,\n payload,\n result_title,\n result_subtitle,\n result_icon,\n result_badge,\n result_snapshot,\n primary_link_href,\n primary_link_label,\n embedding <=> $1::vector AS distance\n FROM ${tableIdent}\n WHERE driver_id = $2\n AND tenant_id = $3::uuid\n AND ($4::uuid[] IS NULL OR organization_id = ANY($4::uuid[]))\n AND (\n $5::text[] IS NULL OR entity_id = ANY($5::text[])\n )\n ORDER BY embedding <=> $1::vector\n LIMIT $6\n `,\n params,\n )\n return res.rows.map<VectorDriverQueryResult>((row) => {\n const distance = typeof row.distance === 'number' ? row.distance : Number(row.distance || 1)\n const score = 1 - distance\n return {\n entityId: row.entity_id,\n recordId: row.record_id,\n organizationId: row.organization_id ?? null,\n checksum: row.checksum,\n url: row.url ?? null,\n presenter: parseJsonColumn<VectorResultPresenter>(row.presenter),\n links: parseJsonColumn<VectorLinkDescriptor[]>(row.links),\n payload: parseJsonColumn<Record<string, unknown>>(row.payload),\n resultTitle: row.result_title ?? '',\n resultSubtitle: row.result_subtitle ?? null,\n resultIcon: row.result_icon ?? null,\n resultBadge: row.result_badge ?? null,\n resultSnapshot: row.result_snapshot ?? null,\n primaryLinkHref: row.primary_link_href ?? null,\n primaryLinkLabel: row.primary_link_label ?? null,\n score,\n }\n })\n }\n\n const list = async (params: VectorDriverListParams): Promise<VectorIndexEntry[]> => {\n await ensureReady()\n const limit = Math.max(1, Math.min(params.limit ?? 50, 200))\n const offset = Math.max(0, params.offset ?? 0)\n const orderColumn = params.orderBy === 'created' ? 'created_at' : 'updated_at'\n const conditions: string[] = [\n 'driver_id = $1',\n 'tenant_id = $2::uuid',\n ]\n const values: any[] = [DRIVER_ID, params.tenantId]\n let nextParam = 3\n\n const normalizedOrganizationId =\n typeof params.organizationId === 'string' && params.organizationId.trim().length > 0\n ? params.organizationId.trim()\n : null\n if (normalizedOrganizationId !== null) {\n conditions.push(`organization_id = $${nextParam}::uuid`)\n values.push(normalizedOrganizationId)\n nextParam += 1\n }\n\n if (params.entityId) {\n conditions.push(`entity_id = $${nextParam}::text`)\n values.push(params.entityId)\n nextParam += 1\n }\n\n const limitParam = nextParam\n const offsetParam = nextParam + 1\n values.push(limit, offset)\n\n const sql = `\n SELECT\n entity_id,\n record_id,\n tenant_id,\n organization_id,\n checksum,\n url,\n presenter,\n links,\n payload,\n result_title,\n result_subtitle,\n result_icon,\n result_badge,\n result_snapshot,\n primary_link_href,\n primary_link_label,\n created_at,\n updated_at\n FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n ORDER BY ${orderColumn} DESC\n LIMIT $${limitParam} OFFSET $${offsetParam}\n `\n\n const res = await pool.query<{\n entity_id: string\n record_id: string\n tenant_id: string\n organization_id: string | null\n checksum: string\n url: string | null\n presenter: string | null\n links: string | null\n payload: string | null\n result_title: string | null\n result_subtitle: string | null\n result_icon: string | null\n result_badge: string | null\n result_snapshot: string | null\n primary_link_href: string | null\n primary_link_label: string | null\n created_at: Date | string\n updated_at: Date | string\n }>(sql, values)\n return res.rows.map<VectorIndexEntry>((row) => {\n const presenter = parseJsonColumn<VectorResultPresenter>(row.presenter)\n const links = parseJsonColumn<VectorLinkDescriptor[]>(row.links)\n const payload = parseJsonColumn<Record<string, unknown>>(row.payload)\n const createdAt =\n row.created_at instanceof Date\n ? row.created_at.toISOString()\n : new Date(row.created_at ?? Date.now()).toISOString()\n const updatedAt =\n row.updated_at instanceof Date\n ? row.updated_at.toISOString()\n : new Date(row.updated_at ?? Date.now()).toISOString()\n return {\n entityId: row.entity_id,\n recordId: row.record_id,\n driverId: DRIVER_ID,\n tenantId: row.tenant_id,\n organizationId: row.organization_id ?? null,\n checksum: row.checksum,\n url: row.url ?? null,\n presenter,\n links,\n payload,\n metadata: payload,\n resultTitle: row.result_title ?? '',\n resultSubtitle: row.result_subtitle ?? null,\n resultIcon: row.result_icon ?? null,\n resultBadge: row.result_badge ?? null,\n resultSnapshot: row.result_snapshot ?? null,\n primaryLinkHref: row.primary_link_href ?? null,\n primaryLinkLabel: row.primary_link_label ?? null,\n createdAt,\n updatedAt,\n score: null,\n }\n })\n }\n\n const count = async (params: VectorDriverCountParams): Promise<number> => {\n await ensureReady()\n const conditions: string[] = [\n 'driver_id = $1',\n 'tenant_id = $2::uuid',\n ]\n const values: any[] = [DRIVER_ID, params.tenantId]\n let nextParam = 3\n\n const normalizedOrganizationId =\n typeof params.organizationId === 'string' && params.organizationId.trim().length > 0\n ? params.organizationId.trim()\n : null\n if (normalizedOrganizationId !== null) {\n conditions.push(`organization_id = $${nextParam}::uuid`)\n values.push(normalizedOrganizationId)\n nextParam += 1\n }\n if (params.entityId) {\n conditions.push(`entity_id = $${nextParam}::text`)\n values.push(params.entityId)\n nextParam += 1\n }\n\n const sql = `\n SELECT count(*)::bigint AS total\n FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n `\n const res = await pool.query<{ total: string }>(sql, values)\n const raw = res.rows?.[0]?.total\n if (!raw) return 0\n const parsed = Number(raw)\n return Number.isFinite(parsed) ? parsed : 0\n }\n\n const removeOrphans = async (params: VectorDriverRemoveOrphansParams): Promise<number> => {\n await ensureReady()\n const conditions: string[] = [\n 'driver_id = $1',\n 'entity_id = $2',\n 'updated_at < $3::timestamptz',\n ]\n const values: any[] = [DRIVER_ID, params.entityId, (params.olderThan instanceof Date ? params.olderThan : new Date(params.olderThan)).toISOString()]\n let nextParam = 4\n\n if (params.tenantId !== undefined) {\n conditions.push(`tenant_id is not distinct from $${nextParam}::uuid`)\n values.push(params.tenantId)\n nextParam += 1\n }\n\n if (params.organizationId !== undefined) {\n conditions.push(`organization_id is not distinct from $${nextParam}::uuid`)\n values.push(params.organizationId)\n nextParam += 1\n }\n\n const sql = `\n DELETE FROM ${tableIdent}\n WHERE ${conditions.join('\\n AND ')}\n `\n const res = await pool.query(sql, values)\n return res.rowCount ?? 0\n }\n\n const getTableDimension = async (): Promise<number | null> => {\n try {\n const res = await pool.query<{ atttypmod: number }>(\n `SELECT a.atttypmod\n FROM pg_attribute a\n JOIN pg_class c ON a.attrelid = c.oid\n WHERE c.relname = $1\n AND a.attname = 'embedding'\n AND a.atttypmod > 0`,\n [tableName]\n )\n if (res.rows.length > 0 && res.rows[0].atttypmod > 0) {\n return res.rows[0].atttypmod\n }\n return null\n } catch {\n return null\n }\n }\n\n const recreateWithDimension = async (newDimension: number): Promise<void> => {\n await withClient(pool, async (client) => {\n await client.query(`DROP TABLE IF EXISTS ${tableIdent} CASCADE`)\n await client.query(`DROP TABLE IF EXISTS ${migrationsIdent} CASCADE`)\n })\n ready = null\n dimension = newDimension\n await ensureReady()\n }\n\n return {\n id: 'pgvector',\n ensureReady,\n upsert,\n delete: remove,\n getChecksum,\n purge,\n query,\n list,\n count,\n removeOrphans,\n getTableDimension,\n recreateWithDimension,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/vector/types.ts"],
4
- "sourcesContent": ["import type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type {\n VectorDriverId,\n VectorIndexSource,\n VectorModuleConfig,\n VectorEntityConfig,\n VectorLinkDescriptor,\n VectorResultPresenter,\n VectorSearchHit,\n VectorQueryRequest,\n VectorIndexEntry,\n} from '@open-mercato/shared/modules/vector'\n\nexport type {\n VectorDriverId,\n VectorIndexSource,\n VectorModuleConfig,\n VectorEntityConfig,\n VectorLinkDescriptor,\n VectorResultPresenter,\n VectorSearchHit,\n VectorQueryRequest,\n VectorIndexEntry,\n}\n\nexport type VectorDriverDocument = {\n entityId: EntityId\n recordId: string\n tenantId: string\n organizationId?: string | null\n checksum: string\n embedding: number[]\n url?: string | null\n presenter?: VectorResultPresenter | null\n links?: VectorLinkDescriptor[] | null\n payload?: Record<string, unknown> | null\n driverId: VectorDriverId\n resultTitle: string\n resultSubtitle?: string | null\n resultIcon?: string | null\n resultBadge?: string | null\n resultSnapshot?: string | null\n primaryLinkHref?: string | null\n primaryLinkLabel?: string | null\n}\n\nexport type VectorDriverQuery = {\n vector: number[]\n limit?: number\n filter?: {\n entityIds?: EntityId[]\n organizationId?: string | null\n tenantId: string\n }\n}\n\nexport type VectorDriverQueryResult = {\n entityId: EntityId\n recordId: string\n organizationId?: string | null\n score: number\n checksum: string\n url?: string | null\n presenter?: VectorResultPresenter | null\n links?: VectorLinkDescriptor[] | null\n payload?: Record<string, unknown> | null\n resultTitle: string\n resultSubtitle?: string | null\n resultIcon?: string | null\n resultBadge?: string | null\n resultSnapshot?: string | null\n primaryLinkHref?: string | null\n primaryLinkLabel?: string | null\n}\n\nexport type VectorDriverListParams = {\n tenantId: string\n organizationId?: string | null\n entityId?: EntityId\n limit?: number\n offset?: number\n orderBy?: 'created' | 'updated'\n}\n\nexport type VectorDriverCountParams = {\n tenantId: string\n organizationId?: string | null\n entityId?: EntityId\n}\n\nexport type VectorDriverRemoveOrphansParams = {\n entityId: EntityId\n tenantId?: string | null\n organizationId?: string | null\n olderThan: Date\n}\n\nexport interface VectorDriver {\n readonly id: VectorDriverId\n ensureReady(): Promise<void>\n upsert(doc: VectorDriverDocument): Promise<void>\n delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void>\n query(input: VectorDriverQuery): Promise<VectorDriverQueryResult[]>\n getChecksum(entityId: EntityId, recordId: string, tenantId: string): Promise<string | null>\n purge?(entityId: EntityId, tenantId: string): Promise<void>\n list?(params: VectorDriverListParams): Promise<VectorIndexEntry[]>\n count?(params: VectorDriverCountParams): Promise<number>\n removeOrphans?(params: VectorDriverRemoveOrphansParams): Promise<number | void>\n getTableDimension?(): Promise<number | null>\n recreateWithDimension?(newDimension: number): Promise<void>\n}\n\n// ============================================================================\n// Embedding Provider Configuration Types\n// ============================================================================\n\nexport type EmbeddingProviderId =\n | 'openai'\n | 'google'\n | 'mistral'\n | 'cohere'\n | 'bedrock'\n | 'ollama'\n\nexport type EmbeddingProviderConfig = {\n providerId: EmbeddingProviderId\n model: string\n dimension: number\n outputDimensionality?: number\n baseUrl?: string\n updatedAt: string\n}\n\nexport type EmbeddingModelInfo = {\n id: string\n name: string\n dimension: number\n configurableDimension?: boolean\n minDimension?: number\n maxDimension?: number\n}\n\nexport type EmbeddingProviderInfo = {\n name: string\n envKeyRequired: string\n defaultModel: string\n models: EmbeddingModelInfo[]\n}\n\nexport const EMBEDDING_PROVIDERS: Record<EmbeddingProviderId, EmbeddingProviderInfo> = {\n openai: {\n name: 'OpenAI',\n envKeyRequired: 'OPENAI_API_KEY',\n defaultModel: 'text-embedding-3-small',\n models: [\n { id: 'text-embedding-3-small', name: 'text-embedding-3-small', dimension: 1536 },\n { id: 'text-embedding-3-large', name: 'text-embedding-3-large', dimension: 3072, configurableDimension: true, minDimension: 256, maxDimension: 3072 },\n { id: 'text-embedding-ada-002', name: 'text-embedding-ada-002', dimension: 1536 },\n ],\n },\n google: {\n name: 'Google Generative AI',\n envKeyRequired: 'GOOGLE_GENERATIVE_AI_API_KEY',\n defaultModel: 'text-embedding-004',\n models: [\n { id: 'text-embedding-004', name: 'text-embedding-004', dimension: 768, configurableDimension: true, minDimension: 1, maxDimension: 768 },\n { id: 'embedding-001', name: 'embedding-001', dimension: 768 },\n ],\n },\n mistral: {\n name: 'Mistral',\n envKeyRequired: 'MISTRAL_API_KEY',\n defaultModel: 'mistral-embed',\n models: [\n { id: 'mistral-embed', name: 'mistral-embed', dimension: 1024 },\n ],\n },\n cohere: {\n name: 'Cohere',\n envKeyRequired: 'COHERE_API_KEY',\n defaultModel: 'embed-english-v3.0',\n models: [\n { id: 'embed-english-v3.0', name: 'embed-english-v3.0', dimension: 1024 },\n { id: 'embed-multilingual-v3.0', name: 'embed-multilingual-v3.0', dimension: 1024 },\n { id: 'embed-english-light-v3.0', name: 'embed-english-light-v3.0', dimension: 384 },\n { id: 'embed-multilingual-light-v3.0', name: 'embed-multilingual-light-v3.0', dimension: 384 },\n ],\n },\n bedrock: {\n name: 'Amazon Bedrock',\n envKeyRequired: 'AWS_ACCESS_KEY_ID',\n defaultModel: 'amazon.titan-embed-text-v2:0',\n models: [\n { id: 'amazon.titan-embed-text-v2:0', name: 'Titan Embed Text v2', dimension: 1024, configurableDimension: true, minDimension: 256, maxDimension: 1024 },\n { id: 'amazon.titan-embed-text-v1', name: 'Titan Embed Text v1', dimension: 1536 },\n { id: 'cohere.embed-english-v3', name: 'Cohere Embed English v3', dimension: 1024 },\n { id: 'cohere.embed-multilingual-v3', name: 'Cohere Embed Multilingual v3', dimension: 1024 },\n ],\n },\n ollama: {\n name: 'Ollama (Local)',\n envKeyRequired: 'OLLAMA_BASE_URL',\n defaultModel: 'nomic-embed-text',\n models: [\n { id: 'nomic-embed-text', name: 'nomic-embed-text', dimension: 768 },\n { id: 'mxbai-embed-large', name: 'mxbai-embed-large', dimension: 1024 },\n { id: 'all-minilm', name: 'all-minilm', dimension: 384 },\n { id: 'snowflake-arctic-embed', name: 'snowflake-arctic-embed', dimension: 1024 },\n ],\n },\n}\n\nexport const EMBEDDING_CONFIG_KEY = 'embedding_provider'\n\nexport const DEFAULT_EMBEDDING_CONFIG: EmbeddingProviderConfig = {\n providerId: 'openai',\n model: 'text-embedding-3-small',\n dimension: 1536,\n updatedAt: new Date().toISOString(),\n}\n"],
5
- "mappings": "AAqJO,MAAM,sBAA0E;AAAA,EACrF,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,KAAK;AAAA,MAChF,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,MAAM,uBAAuB,MAAM,cAAc,KAAK,cAAc,KAAK;AAAA,MACpJ,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,KAAK;AAAA,IAClF;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,sBAAsB,MAAM,sBAAsB,WAAW,KAAK,uBAAuB,MAAM,cAAc,GAAG,cAAc,IAAI;AAAA,MACxI,EAAE,IAAI,iBAAiB,MAAM,iBAAiB,WAAW,IAAI;AAAA,IAC/D;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,iBAAiB,MAAM,iBAAiB,WAAW,KAAK;AAAA,IAChE;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,sBAAsB,MAAM,sBAAsB,WAAW,KAAK;AAAA,MACxE,EAAE,IAAI,2BAA2B,MAAM,2BAA2B,WAAW,KAAK;AAAA,MAClF,EAAE,IAAI,4BAA4B,MAAM,4BAA4B,WAAW,IAAI;AAAA,MACnF,EAAE,IAAI,iCAAiC,MAAM,iCAAiC,WAAW,IAAI;AAAA,IAC/F;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,gCAAgC,MAAM,uBAAuB,WAAW,MAAM,uBAAuB,MAAM,cAAc,KAAK,cAAc,KAAK;AAAA,MACvJ,EAAE,IAAI,8BAA8B,MAAM,uBAAuB,WAAW,KAAK;AAAA,MACjF,EAAE,IAAI,2BAA2B,MAAM,2BAA2B,WAAW,KAAK;AAAA,MAClF,EAAE,IAAI,gCAAgC,MAAM,gCAAgC,WAAW,KAAK;AAAA,IAC9F;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,oBAAoB,MAAM,oBAAoB,WAAW,IAAI;AAAA,MACnE,EAAE,IAAI,qBAAqB,MAAM,qBAAqB,WAAW,KAAK;AAAA,MACtE,EAAE,IAAI,cAAc,MAAM,cAAc,WAAW,IAAI;AAAA,MACvD,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,KAAK;AAAA,IAClF;AAAA,EACF;AACF;AAEO,MAAM,uBAAuB;AAE7B,MAAM,2BAAoD;AAAA,EAC/D,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,EACX,YAAW,oBAAI,KAAK,GAAE,YAAY;AACpC;",
4
+ "sourcesContent": ["import type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type {\n VectorDriverId,\n VectorIndexSource,\n VectorModuleConfig,\n VectorEntityConfig,\n VectorLinkDescriptor,\n VectorResultPresenter,\n VectorSearchHit,\n VectorQueryRequest,\n VectorIndexEntry,\n} from '@open-mercato/shared/modules/vector'\n\nexport type {\n VectorDriverId,\n VectorIndexSource,\n VectorModuleConfig,\n VectorEntityConfig,\n VectorLinkDescriptor,\n VectorResultPresenter,\n VectorSearchHit,\n VectorQueryRequest,\n VectorIndexEntry,\n}\n\nexport type VectorDriverDocument = {\n entityId: EntityId\n recordId: string\n tenantId: string\n organizationId?: string | null\n checksum: string\n embedding: number[]\n url?: string | null\n presenter?: VectorResultPresenter | null\n links?: VectorLinkDescriptor[] | null\n payload?: Record<string, unknown> | null\n driverId: VectorDriverId\n resultTitle: string\n resultSubtitle?: string | null\n resultIcon?: string | null\n resultBadge?: string | null\n resultSnapshot?: string | null\n primaryLinkHref?: string | null\n primaryLinkLabel?: string | null\n}\n\nexport type VectorDriverQuery = {\n vector: number[]\n limit?: number\n filter?: {\n entityIds?: EntityId[]\n organizationId?: string | null\n organizationIds?: string[] | null\n tenantId: string\n }\n}\n\nexport type VectorDriverQueryResult = {\n entityId: EntityId\n recordId: string\n organizationId?: string | null\n score: number\n checksum: string\n url?: string | null\n presenter?: VectorResultPresenter | null\n links?: VectorLinkDescriptor[] | null\n payload?: Record<string, unknown> | null\n resultTitle: string\n resultSubtitle?: string | null\n resultIcon?: string | null\n resultBadge?: string | null\n resultSnapshot?: string | null\n primaryLinkHref?: string | null\n primaryLinkLabel?: string | null\n}\n\nexport type VectorDriverListParams = {\n tenantId: string\n organizationId?: string | null\n entityId?: EntityId\n limit?: number\n offset?: number\n orderBy?: 'created' | 'updated'\n}\n\nexport type VectorDriverCountParams = {\n tenantId: string\n organizationId?: string | null\n entityId?: EntityId\n}\n\nexport type VectorDriverRemoveOrphansParams = {\n entityId: EntityId\n tenantId?: string | null\n organizationId?: string | null\n olderThan: Date\n}\n\nexport interface VectorDriver {\n readonly id: VectorDriverId\n ensureReady(): Promise<void>\n upsert(doc: VectorDriverDocument): Promise<void>\n delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void>\n query(input: VectorDriverQuery): Promise<VectorDriverQueryResult[]>\n getChecksum(entityId: EntityId, recordId: string, tenantId: string): Promise<string | null>\n purge?(entityId: EntityId, tenantId: string): Promise<void>\n list?(params: VectorDriverListParams): Promise<VectorIndexEntry[]>\n count?(params: VectorDriverCountParams): Promise<number>\n removeOrphans?(params: VectorDriverRemoveOrphansParams): Promise<number | void>\n getTableDimension?(): Promise<number | null>\n recreateWithDimension?(newDimension: number): Promise<void>\n}\n\n// ============================================================================\n// Embedding Provider Configuration Types\n// ============================================================================\n\nexport type EmbeddingProviderId =\n | 'openai'\n | 'google'\n | 'mistral'\n | 'cohere'\n | 'bedrock'\n | 'ollama'\n\nexport type EmbeddingProviderConfig = {\n providerId: EmbeddingProviderId\n model: string\n dimension: number\n outputDimensionality?: number\n baseUrl?: string\n updatedAt: string\n}\n\nexport type EmbeddingModelInfo = {\n id: string\n name: string\n dimension: number\n configurableDimension?: boolean\n minDimension?: number\n maxDimension?: number\n}\n\nexport type EmbeddingProviderInfo = {\n name: string\n envKeyRequired: string\n defaultModel: string\n models: EmbeddingModelInfo[]\n}\n\nexport const EMBEDDING_PROVIDERS: Record<EmbeddingProviderId, EmbeddingProviderInfo> = {\n openai: {\n name: 'OpenAI',\n envKeyRequired: 'OPENAI_API_KEY',\n defaultModel: 'text-embedding-3-small',\n models: [\n { id: 'text-embedding-3-small', name: 'text-embedding-3-small', dimension: 1536 },\n { id: 'text-embedding-3-large', name: 'text-embedding-3-large', dimension: 3072, configurableDimension: true, minDimension: 256, maxDimension: 3072 },\n { id: 'text-embedding-ada-002', name: 'text-embedding-ada-002', dimension: 1536 },\n ],\n },\n google: {\n name: 'Google Generative AI',\n envKeyRequired: 'GOOGLE_GENERATIVE_AI_API_KEY',\n defaultModel: 'text-embedding-004',\n models: [\n { id: 'text-embedding-004', name: 'text-embedding-004', dimension: 768, configurableDimension: true, minDimension: 1, maxDimension: 768 },\n { id: 'embedding-001', name: 'embedding-001', dimension: 768 },\n ],\n },\n mistral: {\n name: 'Mistral',\n envKeyRequired: 'MISTRAL_API_KEY',\n defaultModel: 'mistral-embed',\n models: [\n { id: 'mistral-embed', name: 'mistral-embed', dimension: 1024 },\n ],\n },\n cohere: {\n name: 'Cohere',\n envKeyRequired: 'COHERE_API_KEY',\n defaultModel: 'embed-english-v3.0',\n models: [\n { id: 'embed-english-v3.0', name: 'embed-english-v3.0', dimension: 1024 },\n { id: 'embed-multilingual-v3.0', name: 'embed-multilingual-v3.0', dimension: 1024 },\n { id: 'embed-english-light-v3.0', name: 'embed-english-light-v3.0', dimension: 384 },\n { id: 'embed-multilingual-light-v3.0', name: 'embed-multilingual-light-v3.0', dimension: 384 },\n ],\n },\n bedrock: {\n name: 'Amazon Bedrock',\n envKeyRequired: 'AWS_ACCESS_KEY_ID',\n defaultModel: 'amazon.titan-embed-text-v2:0',\n models: [\n { id: 'amazon.titan-embed-text-v2:0', name: 'Titan Embed Text v2', dimension: 1024, configurableDimension: true, minDimension: 256, maxDimension: 1024 },\n { id: 'amazon.titan-embed-text-v1', name: 'Titan Embed Text v1', dimension: 1536 },\n { id: 'cohere.embed-english-v3', name: 'Cohere Embed English v3', dimension: 1024 },\n { id: 'cohere.embed-multilingual-v3', name: 'Cohere Embed Multilingual v3', dimension: 1024 },\n ],\n },\n ollama: {\n name: 'Ollama (Local)',\n envKeyRequired: 'OLLAMA_BASE_URL',\n defaultModel: 'nomic-embed-text',\n models: [\n { id: 'nomic-embed-text', name: 'nomic-embed-text', dimension: 768 },\n { id: 'mxbai-embed-large', name: 'mxbai-embed-large', dimension: 1024 },\n { id: 'all-minilm', name: 'all-minilm', dimension: 384 },\n { id: 'snowflake-arctic-embed', name: 'snowflake-arctic-embed', dimension: 1024 },\n ],\n },\n}\n\nexport const EMBEDDING_CONFIG_KEY = 'embedding_provider'\n\nexport const DEFAULT_EMBEDDING_CONFIG: EmbeddingProviderConfig = {\n providerId: 'openai',\n model: 'text-embedding-3-small',\n dimension: 1536,\n updatedAt: new Date().toISOString(),\n}\n"],
5
+ "mappings": "AAsJO,MAAM,sBAA0E;AAAA,EACrF,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,KAAK;AAAA,MAChF,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,MAAM,uBAAuB,MAAM,cAAc,KAAK,cAAc,KAAK;AAAA,MACpJ,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,KAAK;AAAA,IAClF;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,sBAAsB,MAAM,sBAAsB,WAAW,KAAK,uBAAuB,MAAM,cAAc,GAAG,cAAc,IAAI;AAAA,MACxI,EAAE,IAAI,iBAAiB,MAAM,iBAAiB,WAAW,IAAI;AAAA,IAC/D;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,iBAAiB,MAAM,iBAAiB,WAAW,KAAK;AAAA,IAChE;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,sBAAsB,MAAM,sBAAsB,WAAW,KAAK;AAAA,MACxE,EAAE,IAAI,2BAA2B,MAAM,2BAA2B,WAAW,KAAK;AAAA,MAClF,EAAE,IAAI,4BAA4B,MAAM,4BAA4B,WAAW,IAAI;AAAA,MACnF,EAAE,IAAI,iCAAiC,MAAM,iCAAiC,WAAW,IAAI;AAAA,IAC/F;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,gCAAgC,MAAM,uBAAuB,WAAW,MAAM,uBAAuB,MAAM,cAAc,KAAK,cAAc,KAAK;AAAA,MACvJ,EAAE,IAAI,8BAA8B,MAAM,uBAAuB,WAAW,KAAK;AAAA,MACjF,EAAE,IAAI,2BAA2B,MAAM,2BAA2B,WAAW,KAAK;AAAA,MAClF,EAAE,IAAI,gCAAgC,MAAM,gCAAgC,WAAW,KAAK;AAAA,IAC9F;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,EAAE,IAAI,oBAAoB,MAAM,oBAAoB,WAAW,IAAI;AAAA,MACnE,EAAE,IAAI,qBAAqB,MAAM,qBAAqB,WAAW,KAAK;AAAA,MACtE,EAAE,IAAI,cAAc,MAAM,cAAc,WAAW,IAAI;AAAA,MACvD,EAAE,IAAI,0BAA0B,MAAM,0BAA0B,WAAW,KAAK;AAAA,IAClF;AAAA,EACF;AACF;AAEO,MAAM,uBAAuB;AAE7B,MAAM,2BAAoD;AAAA,EAC/D,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,EACX,YAAW,oBAAI,KAAK,GAAE,YAAY;AACpC;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/search",
3
- "version": "0.6.4-develop.4239.1.4a264a5828",
3
+ "version": "0.6.4-develop.4264.1.53368d85fe",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -126,9 +126,9 @@
126
126
  "zod": "^4.4.3"
127
127
  },
128
128
  "peerDependencies": {
129
- "@open-mercato/core": "0.6.4-develop.4239.1.4a264a5828",
130
- "@open-mercato/queue": "0.6.4-develop.4239.1.4a264a5828",
131
- "@open-mercato/shared": "0.6.4-develop.4239.1.4a264a5828"
129
+ "@open-mercato/core": "0.6.4-develop.4264.1.53368d85fe",
130
+ "@open-mercato/queue": "0.6.4-develop.4264.1.53368d85fe",
131
+ "@open-mercato/shared": "0.6.4-develop.4264.1.53368d85fe"
132
132
  },
133
133
  "devDependencies": {
134
134
  "@types/jest": "^30.0.0",
@@ -232,6 +232,50 @@ describe('SearchService', () => {
232
232
  expect(results[0].source).toBe('fallback')
233
233
  })
234
234
 
235
+ it('should drop cross-organization results for scoped multi-org searches', async () => {
236
+ const strategy = createMockStrategy({
237
+ id: 'test',
238
+ search: jest.fn().mockResolvedValue([
239
+ createMockResult({ recordId: 'rec-a', organizationId: 'org-A' }),
240
+ createMockResult({ recordId: 'rec-b', organizationId: 'org-B' }),
241
+ createMockResult({ recordId: 'rec-c', organizationId: 'org-C' }),
242
+ createMockResult({ recordId: 'rec-unknown', organizationId: undefined }),
243
+ ]),
244
+ })
245
+ const service = new SearchService({
246
+ strategies: [strategy],
247
+ defaultStrategies: ['test'],
248
+ })
249
+
250
+ const results = await service.search('test', {
251
+ tenantId: 'tenant-123',
252
+ organizationIds: ['org-A', 'org-B'],
253
+ })
254
+
255
+ expect(results.map((result) => result.recordId)).toEqual(['rec-a', 'rec-b'])
256
+ })
257
+
258
+ it('should fail closed for an empty organization allowlist', async () => {
259
+ const strategy = createMockStrategy({
260
+ id: 'test',
261
+ search: jest.fn().mockResolvedValue([
262
+ createMockResult({ recordId: 'rec-a', organizationId: 'org-A' }),
263
+ ]),
264
+ })
265
+ const service = new SearchService({
266
+ strategies: [strategy],
267
+ defaultStrategies: ['test'],
268
+ })
269
+
270
+ const results = await service.search('test', {
271
+ tenantId: 'tenant-123',
272
+ organizationIds: [],
273
+ })
274
+
275
+ expect(results).toEqual([])
276
+ expect(strategy.search).not.toHaveBeenCalled()
277
+ })
278
+
235
279
  it('should enrich results when navigation metadata is missing even if presenter title exists', async () => {
236
280
  const strategy = createMockStrategy({
237
281
  id: 'test',
@@ -66,8 +66,21 @@ export function createMeilisearchDriver(
66
66
  function buildFilters(options: FullTextSearchQuery): string[] {
67
67
  const filters: string[] = []
68
68
 
69
- if (options.organizationId) {
70
- filters.push(`_organizationId = "${escapeFilterValue(options.organizationId)}"`)
69
+ const organizationId = typeof options.organizationId === 'string' ? options.organizationId.trim() : ''
70
+ if (organizationId) {
71
+ filters.push(`_organizationId = "${escapeFilterValue(organizationId)}"`)
72
+ } else if (Array.isArray(options.organizationIds)) {
73
+ const organizationIds = Array.from(new Set(
74
+ options.organizationIds
75
+ .map((value) => (typeof value === 'string' ? value.trim() : ''))
76
+ .filter((value) => value.length > 0),
77
+ ))
78
+ if (organizationIds.length === 0) {
79
+ filters.push('_organizationId = "__open_mercato_no_matching_organization__"')
80
+ } else {
81
+ const orgFilter = organizationIds.map((id) => `"${escapeFilterValue(id)}"`).join(', ')
82
+ filters.push(`_organizationId IN [${orgFilter}]`)
83
+ }
71
84
  }
72
85
 
73
86
  if (options.entityTypes?.length) {
@@ -216,6 +229,7 @@ export function createMeilisearchDriver(
216
229
  recordId: hit._id as string,
217
230
  entityId: hit._entityId as EntityId,
218
231
  score: (hit._rankingScore as number) ?? 0.5,
232
+ organizationId: hit._organizationId as string | null | undefined,
219
233
  presenter: hit._presenter as FullTextSearchHit['presenter'],
220
234
  url: hit._url as string | undefined,
221
235
  links: hit._links as FullTextSearchHit['links'],
@@ -39,6 +39,7 @@ export type FullTextSearchDocument = {
39
39
  export type FullTextSearchQuery = {
40
40
  tenantId: string
41
41
  organizationId?: string | null
42
+ organizationIds?: string[] | null
42
43
  entityTypes?: EntityId[]
43
44
  limit?: number
44
45
  offset?: number
@@ -52,6 +53,7 @@ export type FullTextSearchHit = {
52
53
  recordId: string
53
54
  entityId: EntityId
54
55
  score: number
56
+ organizationId?: string | null
55
57
  presenter?: SearchResultPresenter
56
58
  url?: string
57
59
  links?: SearchResultLink[]
package/src/lib/merger.ts CHANGED
@@ -65,15 +65,22 @@ export function mergeAndRankResults(
65
65
  presenter: result.presenter,
66
66
  url: existing.result.url ?? result.url,
67
67
  links: existing.result.links ?? result.links,
68
+ organizationId: existing.result.organizationId ?? result.organizationId,
68
69
  }
69
70
  existing.bestContribution = Math.max(existing.bestContribution, rrfScore)
70
71
  } else if (hasExistingPresenter && hasNewPresenter && rrfScore > existing.bestContribution) {
71
72
  // Both have presenter, keep the one with better RRF contribution (not raw score)
72
- existing.result = { ...result }
73
+ existing.result = {
74
+ ...result,
75
+ organizationId: result.organizationId ?? existing.result.organizationId,
76
+ }
73
77
  existing.bestContribution = rrfScore
74
78
  } else if (!hasExistingPresenter && !hasNewPresenter && rrfScore > existing.bestContribution) {
75
79
  // Neither has presenter, keep result with better RRF contribution
76
- existing.result = { ...result }
80
+ existing.result = {
81
+ ...result,
82
+ organizationId: result.organizationId ?? existing.result.organizationId,
83
+ }
77
84
  existing.bestContribution = rrfScore
78
85
  }
79
86
  // If existing has presenter and new doesn't, keep existing (do nothing)