@open-mercato/search 0.6.8-develop.7100.1.fbf66fca35 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +0 -1
- package/dist/indexer/search-indexer.js +0 -66
- package/dist/indexer/search-indexer.js.map +2 -2
- package/dist/lib/presenter-enricher.js +1 -71
- package/dist/lib/presenter-enricher.js.map +2 -2
- package/dist/modules/search/__integration__/TC-SEARCH-006.spec.js +0 -143
- package/dist/modules/search/__integration__/TC-SEARCH-006.spec.js.map +2 -2
- package/dist/modules/search/lib/entity-access.js +43 -1
- package/dist/modules/search/lib/entity-access.js.map +2 -2
- package/dist/modules/search/workers/fulltext-index.worker.js +24 -7
- package/dist/modules/search/workers/fulltext-index.worker.js.map +2 -2
- package/dist/service.js +1 -18
- package/dist/service.js.map +2 -2
- package/dist/strategies/token.strategy.js +2 -8
- package/dist/strategies/token.strategy.js.map +2 -2
- package/package.json +5 -6
- package/src/__tests__/presenter-enricher.test.ts +0 -234
- package/src/__tests__/service.test.ts +0 -24
- package/src/__tests__/workers.test.ts +17 -46
- package/src/indexer/search-indexer.ts +0 -84
- package/src/lib/presenter-enricher.ts +1 -92
- package/src/modules/search/__integration__/TC-SEARCH-006.spec.ts +2 -190
- package/src/modules/search/api/__tests__/global-search.routes.test.ts +0 -107
- package/src/modules/search/lib/entity-access.ts +130 -4
- package/src/modules/search/workers/fulltext-index.worker.ts +29 -13
- package/src/service.ts +2 -37
- package/src/strategies/token.strategy.ts +2 -15
- package/src/__tests__/search-indexer-batch.test.ts +0 -214
- package/src/__tests__/token-strategy-entity-exclusion.test.ts +0 -99
package/src/service.ts
CHANGED
|
@@ -25,38 +25,6 @@ const DEFAULT_MERGE_CONFIG: ResultMergeConfig = {
|
|
|
25
25
|
*/
|
|
26
26
|
const STRATEGY_AVAILABILITY_CACHE_TTL_MS = 2_000
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* Maximum records indexed at once when bulkIndex falls back to per-record writes
|
|
30
|
-
* for a strategy that has no bulkIndex implementation (currently the vector
|
|
31
|
-
* strategy, whose index() performs an embedding-provider round trip per record).
|
|
32
|
-
* A whole reindex page arrives in one bulkIndex call, so an unbounded fan-out
|
|
33
|
-
* would burst hundreds of concurrent provider requests from a single job.
|
|
34
|
-
*/
|
|
35
|
-
const BULK_INDEX_FALLBACK_CONCURRENCY = 4
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Map items through an async worker with a fixed number of in-flight calls.
|
|
39
|
-
* Rejects with the first error, matching Promise.all semantics.
|
|
40
|
-
*/
|
|
41
|
-
async function mapWithConcurrency<T>(
|
|
42
|
-
items: T[],
|
|
43
|
-
limit: number,
|
|
44
|
-
worker: (item: T) => Promise<void>,
|
|
45
|
-
): Promise<void> {
|
|
46
|
-
if (items.length === 0) return
|
|
47
|
-
|
|
48
|
-
let nextIndex = 0
|
|
49
|
-
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
50
|
-
for (;;) {
|
|
51
|
-
const currentIndex = nextIndex++
|
|
52
|
-
if (currentIndex >= items.length) return
|
|
53
|
-
await worker(items[currentIndex])
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
await Promise.all(runners)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
28
|
function normalizeOrganizationFilter(options: SearchOptions): string[] | null {
|
|
61
29
|
const single = typeof options.organizationId === 'string' ? options.organizationId.trim() : ''
|
|
62
30
|
if (single) return [single]
|
|
@@ -267,11 +235,8 @@ export class SearchService {
|
|
|
267
235
|
if (strategy.bulkIndex) {
|
|
268
236
|
return strategy.bulkIndex(records)
|
|
269
237
|
}
|
|
270
|
-
// Fallback to individual indexing
|
|
271
|
-
|
|
272
|
-
return mapWithConcurrency(records, BULK_INDEX_FALLBACK_CONCURRENCY, (record) =>
|
|
273
|
-
this.executeStrategyIndex(strategy, record),
|
|
274
|
-
)
|
|
238
|
+
// Fallback to individual indexing
|
|
239
|
+
return Promise.all(records.map((record) => this.executeStrategyIndex(strategy, record)))
|
|
275
240
|
}),
|
|
276
241
|
)
|
|
277
242
|
|
|
@@ -67,21 +67,10 @@ export class TokenSearchStrategy implements SearchStrategy {
|
|
|
67
67
|
// Dynamically import tokenization to avoid circular dependencies
|
|
68
68
|
const { tokenizeText } = await import('@open-mercato/shared/lib/search/tokenize')
|
|
69
69
|
const { resolveSearchConfig } = await import('@open-mercato/shared/lib/search/config')
|
|
70
|
-
const { listSearchTokenExcludedEntityTypes } = await import(
|
|
71
|
-
'@open-mercato/core/modules/query_index/lib/search-entity-policy'
|
|
72
|
-
)
|
|
73
70
|
|
|
74
71
|
const config = resolveSearchConfig()
|
|
75
72
|
if (!config.enabled) return []
|
|
76
73
|
|
|
77
|
-
// The rows themselves stay in `search_tokens` — list routes and the query engines' encrypted
|
|
78
|
-
// like/ilike rewrite depend on them — so the exclusion is enforced here, at read time.
|
|
79
|
-
const excludedEntityTypes = listSearchTokenExcludedEntityTypes()
|
|
80
|
-
const requestedEntityTypes = options.entityTypes?.length
|
|
81
|
-
? options.entityTypes.filter((entityType) => !excludedEntityTypes.includes(entityType))
|
|
82
|
-
: undefined
|
|
83
|
-
if (options.entityTypes?.length && !requestedEntityTypes?.length) return []
|
|
84
|
-
|
|
85
74
|
const { hashes } = tokenizeText(query, config)
|
|
86
75
|
if (hashes.length === 0) return []
|
|
87
76
|
|
|
@@ -107,10 +96,8 @@ export class TokenSearchStrategy implements SearchStrategy {
|
|
|
107
96
|
queryBuilder = queryBuilder.where('organization_id' as any, 'in', organizationIds)
|
|
108
97
|
}
|
|
109
98
|
|
|
110
|
-
if (
|
|
111
|
-
queryBuilder = queryBuilder.where('entity_type' as any, 'in',
|
|
112
|
-
} else if (excludedEntityTypes.length) {
|
|
113
|
-
queryBuilder = queryBuilder.where('entity_type' as any, 'not in', excludedEntityTypes)
|
|
99
|
+
if (options.entityTypes?.length) {
|
|
100
|
+
queryBuilder = queryBuilder.where('entity_type' as any, 'in', options.entityTypes)
|
|
114
101
|
}
|
|
115
102
|
|
|
116
103
|
const rows = await queryBuilder.execute() as Array<{
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { SearchIndexer } from '../indexer/search-indexer'
|
|
2
|
-
import type { SearchModuleConfig } from '../types'
|
|
3
|
-
import type { QueryEngine, QueryResult } from '@open-mercato/shared/lib/query/types'
|
|
4
|
-
|
|
5
|
-
describe('SearchIndexer.indexRecordsById', () => {
|
|
6
|
-
const moduleConfigs: SearchModuleConfig[] = [
|
|
7
|
-
{
|
|
8
|
-
entities: [
|
|
9
|
-
{
|
|
10
|
-
entityId: 'test:entity',
|
|
11
|
-
enabled: true,
|
|
12
|
-
formatResult: async (ctx) => ({ title: String(ctx.record.name ?? ctx.record.id) }),
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
entityId: 'test:other',
|
|
16
|
-
enabled: true,
|
|
17
|
-
formatResult: async (ctx) => ({ title: String(ctx.record.name ?? ctx.record.id) }),
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
function makeQueryEngine(recordsByEntity: Record<string, Record<string, unknown>[]>): QueryEngine {
|
|
24
|
-
return {
|
|
25
|
-
query: jest.fn(async (entity, opts) => {
|
|
26
|
-
const wantedId = (opts?.filters as { id?: string } | undefined)?.id
|
|
27
|
-
const items = (recordsByEntity[entity as string] ?? []).filter((r) => r.id === wantedId)
|
|
28
|
-
return { items, total: items.length } as QueryResult
|
|
29
|
-
}),
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
it('writes N queued records through exactly one bulkIndex call, not N', async () => {
|
|
34
|
-
const records = [
|
|
35
|
-
{ id: 'rec-1', name: 'Alpha' },
|
|
36
|
-
{ id: 'rec-2', name: 'Beta' },
|
|
37
|
-
{ id: 'rec-3', name: 'Gamma' },
|
|
38
|
-
]
|
|
39
|
-
const searchService = { bulkIndex: jest.fn().mockResolvedValue(undefined) }
|
|
40
|
-
const indexer = new SearchIndexer(searchService as any, moduleConfigs, {
|
|
41
|
-
queryEngine: makeQueryEngine({ 'test:entity': records }),
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const result = await indexer.indexRecordsById({
|
|
45
|
-
items: [
|
|
46
|
-
{ entityId: 'test:entity', recordId: 'rec-1' },
|
|
47
|
-
{ entityId: 'test:entity', recordId: 'rec-2' },
|
|
48
|
-
{ entityId: 'test:entity', recordId: 'rec-3' },
|
|
49
|
-
],
|
|
50
|
-
tenantId: 'tenant-123',
|
|
51
|
-
organizationId: 'org-456',
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
expect(searchService.bulkIndex).toHaveBeenCalledTimes(1)
|
|
55
|
-
expect(searchService.bulkIndex).toHaveBeenCalledWith([
|
|
56
|
-
expect.objectContaining({ entityId: 'test:entity', recordId: 'rec-1', tenantId: 'tenant-123', organizationId: 'org-456' }),
|
|
57
|
-
expect.objectContaining({ entityId: 'test:entity', recordId: 'rec-2', tenantId: 'tenant-123', organizationId: 'org-456' }),
|
|
58
|
-
expect.objectContaining({ entityId: 'test:entity', recordId: 'rec-3', tenantId: 'tenant-123', organizationId: 'org-456' }),
|
|
59
|
-
])
|
|
60
|
-
expect(result).toEqual({ indexed: 3, skipped: 0 })
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('collapses records spanning multiple entities into one bulkIndex call', async () => {
|
|
64
|
-
const searchService = { bulkIndex: jest.fn().mockResolvedValue(undefined) }
|
|
65
|
-
const indexer = new SearchIndexer(searchService as any, moduleConfigs, {
|
|
66
|
-
queryEngine: makeQueryEngine({
|
|
67
|
-
'test:entity': [{ id: 'rec-1', name: 'Alpha' }],
|
|
68
|
-
'test:other': [{ id: 'other-1', name: 'Delta' }],
|
|
69
|
-
}),
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
const result = await indexer.indexRecordsById({
|
|
73
|
-
items: [
|
|
74
|
-
{ entityId: 'test:entity', recordId: 'rec-1' },
|
|
75
|
-
{ entityId: 'test:other', recordId: 'other-1' },
|
|
76
|
-
],
|
|
77
|
-
tenantId: 'tenant-123',
|
|
78
|
-
organizationId: null,
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
expect(searchService.bulkIndex).toHaveBeenCalledTimes(1)
|
|
82
|
-
expect(searchService.bulkIndex).toHaveBeenCalledWith([
|
|
83
|
-
expect.objectContaining({ entityId: 'test:entity', recordId: 'rec-1' }),
|
|
84
|
-
expect.objectContaining({ entityId: 'test:other', recordId: 'other-1' }),
|
|
85
|
-
])
|
|
86
|
-
expect(result).toEqual({ indexed: 2, skipped: 0 })
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('loads each record with custom fields and without triggering auto-reindex', async () => {
|
|
90
|
-
const queryEngine = makeQueryEngine({ 'test:entity': [{ id: 'rec-1', name: 'Alpha' }] })
|
|
91
|
-
const searchService = { bulkIndex: jest.fn().mockResolvedValue(undefined) }
|
|
92
|
-
const indexer = new SearchIndexer(searchService as any, moduleConfigs, { queryEngine })
|
|
93
|
-
|
|
94
|
-
await indexer.indexRecordsById({
|
|
95
|
-
items: [{ entityId: 'test:entity', recordId: 'rec-1' }],
|
|
96
|
-
tenantId: 'tenant-123',
|
|
97
|
-
organizationId: 'org-456',
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
expect(queryEngine.query).toHaveBeenCalledWith(
|
|
101
|
-
'test:entity',
|
|
102
|
-
expect.objectContaining({
|
|
103
|
-
tenantId: 'tenant-123',
|
|
104
|
-
organizationId: 'org-456',
|
|
105
|
-
filters: { id: 'rec-1' },
|
|
106
|
-
includeCustomFields: true,
|
|
107
|
-
skipAutoReindex: true,
|
|
108
|
-
}),
|
|
109
|
-
)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('skips records that no longer exist without failing the batch write', async () => {
|
|
113
|
-
const records = [{ id: 'rec-1', name: 'Alpha' }]
|
|
114
|
-
const searchService = { bulkIndex: jest.fn().mockResolvedValue(undefined) }
|
|
115
|
-
const indexer = new SearchIndexer(searchService as any, moduleConfigs, {
|
|
116
|
-
queryEngine: makeQueryEngine({ 'test:entity': records }),
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
const result = await indexer.indexRecordsById({
|
|
120
|
-
items: [
|
|
121
|
-
{ entityId: 'test:entity', recordId: 'rec-1' },
|
|
122
|
-
{ entityId: 'test:entity', recordId: 'missing' },
|
|
123
|
-
],
|
|
124
|
-
tenantId: 'tenant-123',
|
|
125
|
-
organizationId: null,
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
expect(searchService.bulkIndex).toHaveBeenCalledTimes(1)
|
|
129
|
-
expect(result).toEqual({ indexed: 1, skipped: 1 })
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('keeps indexing the batch when loading one record throws', async () => {
|
|
133
|
-
const searchService = { bulkIndex: jest.fn().mockResolvedValue(undefined) }
|
|
134
|
-
const queryEngine: QueryEngine = {
|
|
135
|
-
query: jest.fn(async (_entity, opts) => {
|
|
136
|
-
const wantedId = (opts?.filters as { id?: string } | undefined)?.id
|
|
137
|
-
if (wantedId === 'boom') throw new Error('connection lost')
|
|
138
|
-
return { items: [{ id: wantedId, name: 'Alpha' }], total: 1 } as QueryResult
|
|
139
|
-
}),
|
|
140
|
-
}
|
|
141
|
-
const indexer = new SearchIndexer(searchService as any, moduleConfigs, { queryEngine })
|
|
142
|
-
|
|
143
|
-
const result = await indexer.indexRecordsById({
|
|
144
|
-
items: [
|
|
145
|
-
{ entityId: 'test:entity', recordId: 'rec-1' },
|
|
146
|
-
{ entityId: 'test:entity', recordId: 'boom' },
|
|
147
|
-
{ entityId: 'test:entity', recordId: 'rec-2' },
|
|
148
|
-
],
|
|
149
|
-
tenantId: 'tenant-123',
|
|
150
|
-
organizationId: null,
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
expect(searchService.bulkIndex).toHaveBeenCalledTimes(1)
|
|
154
|
-
expect(searchService.bulkIndex).toHaveBeenCalledWith([
|
|
155
|
-
expect.objectContaining({ recordId: 'rec-1' }),
|
|
156
|
-
expect.objectContaining({ recordId: 'rec-2' }),
|
|
157
|
-
])
|
|
158
|
-
expect(result).toEqual({ indexed: 2, skipped: 1 })
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('propagates a bulkIndex failure so the queue can retry the job', async () => {
|
|
162
|
-
const searchService = { bulkIndex: jest.fn().mockRejectedValue(new Error('meilisearch unavailable')) }
|
|
163
|
-
const indexer = new SearchIndexer(searchService as any, moduleConfigs, {
|
|
164
|
-
queryEngine: makeQueryEngine({ 'test:entity': [{ id: 'rec-1', name: 'Alpha' }] }),
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
await expect(
|
|
168
|
-
indexer.indexRecordsById({
|
|
169
|
-
items: [{ entityId: 'test:entity', recordId: 'rec-1' }],
|
|
170
|
-
tenantId: 'tenant-123',
|
|
171
|
-
organizationId: null,
|
|
172
|
-
}),
|
|
173
|
-
).rejects.toThrow('meilisearch unavailable')
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('skips entities that are not configured and never calls bulkIndex when nothing is indexable', async () => {
|
|
177
|
-
const searchService = { bulkIndex: jest.fn().mockResolvedValue(undefined) }
|
|
178
|
-
const indexer = new SearchIndexer(searchService as any, moduleConfigs, {
|
|
179
|
-
queryEngine: makeQueryEngine({}),
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
const result = await indexer.indexRecordsById({
|
|
183
|
-
items: [{ entityId: 'unknown:entity', recordId: 'rec-1' }],
|
|
184
|
-
tenantId: 'tenant-123',
|
|
185
|
-
organizationId: null,
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
expect(searchService.bulkIndex).not.toHaveBeenCalled()
|
|
189
|
-
expect(result).toEqual({ indexed: 0, skipped: 1 })
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('counts records dropped for a missing id as skipped so the totals add up', async () => {
|
|
193
|
-
const searchService = { bulkIndex: jest.fn().mockResolvedValue(undefined) }
|
|
194
|
-
const queryEngine: QueryEngine = {
|
|
195
|
-
query: jest.fn(async (_entity, opts) => {
|
|
196
|
-
const wantedId = (opts?.filters as { id?: string } | undefined)?.id
|
|
197
|
-
const item = wantedId === 'no-id' ? { name: 'Ghost' } : { id: wantedId, name: 'Alpha' }
|
|
198
|
-
return { items: [item], total: 1 } as QueryResult
|
|
199
|
-
}),
|
|
200
|
-
}
|
|
201
|
-
const indexer = new SearchIndexer(searchService as any, moduleConfigs, { queryEngine })
|
|
202
|
-
|
|
203
|
-
const result = await indexer.indexRecordsById({
|
|
204
|
-
items: [
|
|
205
|
-
{ entityId: 'test:entity', recordId: 'rec-1' },
|
|
206
|
-
{ entityId: 'test:entity', recordId: 'no-id' },
|
|
207
|
-
],
|
|
208
|
-
tenantId: 'tenant-123',
|
|
209
|
-
organizationId: null,
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
expect(result).toEqual({ indexed: 1, skipped: 1 })
|
|
213
|
-
})
|
|
214
|
-
})
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { TokenSearchStrategy } from '../strategies/token.strategy'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Coverage for `OM_SEARCH_CUSTOMERS_INDEX_BASE_ENTITY` on the read path (#5046).
|
|
5
|
-
*
|
|
6
|
-
* The exclusion is read-side by design and the writer deliberately ignores the flag:
|
|
7
|
-
* `search_tokens` doubles as the encrypted-column lookup index, so the People and Companies list
|
|
8
|
-
* search resolves ids through the very `customers:customer_entity` rows this flag hides from
|
|
9
|
-
* search results. Dropping them write-side would turn that list search into a silent empty page.
|
|
10
|
-
* These tests therefore pin the SQL predicate the strategy issues, never the writer's behavior —
|
|
11
|
-
* `packages/core/src/modules/query_index/__tests__/search-entity-policy.test.ts` is the other half
|
|
12
|
-
* of the pair and asserts the writer emits byte-identical rows in both flag states.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const CUSTOMER_ENTITY = 'customers:customer_entity'
|
|
16
|
-
const PERSON_PROFILE = 'customers:customer_person_profile'
|
|
17
|
-
|
|
18
|
-
type RecordedWhere = [string, string, unknown]
|
|
19
|
-
|
|
20
|
-
function createMockDb() {
|
|
21
|
-
const wheres: RecordedWhere[] = []
|
|
22
|
-
const builder: Record<string, unknown> = {
|
|
23
|
-
select: jest.fn(() => builder),
|
|
24
|
-
where: jest.fn((column: unknown, op?: unknown, value?: unknown) => {
|
|
25
|
-
if (typeof column === 'string' && typeof op === 'string') wheres.push([column, op, value])
|
|
26
|
-
return builder
|
|
27
|
-
}),
|
|
28
|
-
groupBy: jest.fn(() => builder),
|
|
29
|
-
having: jest.fn(() => builder),
|
|
30
|
-
orderBy: jest.fn(() => builder),
|
|
31
|
-
limit: jest.fn(() => builder),
|
|
32
|
-
execute: jest.fn().mockResolvedValue([]),
|
|
33
|
-
}
|
|
34
|
-
const db = { selectFrom: jest.fn(() => builder) }
|
|
35
|
-
return { db, wheres, builder }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const entityTypePredicates = (wheres: RecordedWhere[]) => wheres.filter(([column]) => column === 'entity_type')
|
|
39
|
-
|
|
40
|
-
const originalFlag = process.env.OM_SEARCH_CUSTOMERS_INDEX_BASE_ENTITY
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
if (originalFlag === undefined) delete process.env.OM_SEARCH_CUSTOMERS_INDEX_BASE_ENTITY
|
|
44
|
-
else process.env.OM_SEARCH_CUSTOMERS_INDEX_BASE_ENTITY = originalFlag
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
describe('TokenSearchStrategy excludes base customer entities by default', () => {
|
|
48
|
-
it('adds a NOT IN predicate when the caller requests no specific entity types', async () => {
|
|
49
|
-
delete process.env.OM_SEARCH_CUSTOMERS_INDEX_BASE_ENTITY
|
|
50
|
-
const { db, wheres } = createMockDb()
|
|
51
|
-
const strategy = new TokenSearchStrategy(db as never)
|
|
52
|
-
|
|
53
|
-
await strategy.search('ada lovelace', { tenantId: 'tenant-1' })
|
|
54
|
-
|
|
55
|
-
expect(entityTypePredicates(wheres)).toEqual([['entity_type', 'not in', [CUSTOMER_ENTITY]]])
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('drops the excluded type from an explicit entityTypes filter and keeps the rest', async () => {
|
|
59
|
-
delete process.env.OM_SEARCH_CUSTOMERS_INDEX_BASE_ENTITY
|
|
60
|
-
const { db, wheres } = createMockDb()
|
|
61
|
-
const strategy = new TokenSearchStrategy(db as never)
|
|
62
|
-
|
|
63
|
-
await strategy.search('ada lovelace', {
|
|
64
|
-
tenantId: 'tenant-1',
|
|
65
|
-
entityTypes: [CUSTOMER_ENTITY, PERSON_PROFILE],
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
expect(entityTypePredicates(wheres)).toEqual([['entity_type', 'in', [PERSON_PROFILE]]])
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('returns no results — and issues no query — when only excluded types were requested', async () => {
|
|
72
|
-
delete process.env.OM_SEARCH_CUSTOMERS_INDEX_BASE_ENTITY
|
|
73
|
-
const { db } = createMockDb()
|
|
74
|
-
const strategy = new TokenSearchStrategy(db as never)
|
|
75
|
-
|
|
76
|
-
const results = await strategy.search('ada lovelace', {
|
|
77
|
-
tenantId: 'tenant-1',
|
|
78
|
-
entityTypes: [CUSTOMER_ENTITY],
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
expect(results).toEqual([])
|
|
82
|
-
expect(db.selectFrom).not.toHaveBeenCalled()
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('leaves the query untouched when the flag re-enables base customer entities', async () => {
|
|
86
|
-
process.env.OM_SEARCH_CUSTOMERS_INDEX_BASE_ENTITY = 'true'
|
|
87
|
-
const { db, wheres } = createMockDb()
|
|
88
|
-
const strategy = new TokenSearchStrategy(db as never)
|
|
89
|
-
|
|
90
|
-
await strategy.search('ada lovelace', { tenantId: 'tenant-1' })
|
|
91
|
-
expect(entityTypePredicates(wheres)).toEqual([])
|
|
92
|
-
|
|
93
|
-
await strategy.search('ada lovelace', {
|
|
94
|
-
tenantId: 'tenant-1',
|
|
95
|
-
entityTypes: [CUSTOMER_ENTITY, PERSON_PROFILE],
|
|
96
|
-
})
|
|
97
|
-
expect(entityTypePredicates(wheres)).toEqual([['entity_type', 'in', [CUSTOMER_ENTITY, PERSON_PROFILE]]])
|
|
98
|
-
})
|
|
99
|
-
})
|