@open-mercato/search 0.4.2-canary-c02407ff85
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 +678 -0
- package/build.mjs +92 -0
- package/dist/di.js +157 -0
- package/dist/di.js.map +7 -0
- package/dist/fulltext/drivers/index.js +21 -0
- package/dist/fulltext/drivers/index.js.map +7 -0
- package/dist/fulltext/drivers/meilisearch/index.js +320 -0
- package/dist/fulltext/drivers/meilisearch/index.js.map +7 -0
- package/dist/fulltext/index.js +7 -0
- package/dist/fulltext/index.js.map +7 -0
- package/dist/fulltext/types.js +1 -0
- package/dist/fulltext/types.js.map +7 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +7 -0
- package/dist/indexer/index.js +8 -0
- package/dist/indexer/index.js.map +7 -0
- package/dist/indexer/search-indexer.js +848 -0
- package/dist/indexer/search-indexer.js.map +7 -0
- package/dist/indexer/subscribers/delete.js +41 -0
- package/dist/indexer/subscribers/delete.js.map +7 -0
- package/dist/lib/debug.js +34 -0
- package/dist/lib/debug.js.map +7 -0
- package/dist/lib/fallback-presenter.js +107 -0
- package/dist/lib/fallback-presenter.js.map +7 -0
- package/dist/lib/field-policy.js +75 -0
- package/dist/lib/field-policy.js.map +7 -0
- package/dist/lib/index.js +19 -0
- package/dist/lib/index.js.map +7 -0
- package/dist/lib/merger.js +93 -0
- package/dist/lib/merger.js.map +7 -0
- package/dist/lib/presenter-enricher.js +192 -0
- package/dist/lib/presenter-enricher.js.map +7 -0
- package/dist/modules/search/acl.js +14 -0
- package/dist/modules/search/acl.js.map +7 -0
- package/dist/modules/search/ai-tools.js +284 -0
- package/dist/modules/search/ai-tools.js.map +7 -0
- package/dist/modules/search/api/embeddings/reindex/cancel/route.js +65 -0
- package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +7 -0
- package/dist/modules/search/api/embeddings/reindex/route.js +165 -0
- package/dist/modules/search/api/embeddings/reindex/route.js.map +7 -0
- package/dist/modules/search/api/embeddings/route.js +246 -0
- package/dist/modules/search/api/embeddings/route.js.map +7 -0
- package/dist/modules/search/api/index/route.js +245 -0
- package/dist/modules/search/api/index/route.js.map +7 -0
- package/dist/modules/search/api/reindex/cancel/route.js +65 -0
- package/dist/modules/search/api/reindex/cancel/route.js.map +7 -0
- package/dist/modules/search/api/reindex/route.js +332 -0
- package/dist/modules/search/api/reindex/route.js.map +7 -0
- package/dist/modules/search/api/search/global/route.js +100 -0
- package/dist/modules/search/api/search/global/route.js.map +7 -0
- package/dist/modules/search/api/search/route.js +101 -0
- package/dist/modules/search/api/search/route.js.map +7 -0
- package/dist/modules/search/api/settings/fulltext/route.js +55 -0
- package/dist/modules/search/api/settings/fulltext/route.js.map +7 -0
- package/dist/modules/search/api/settings/global-search/route.js +80 -0
- package/dist/modules/search/api/settings/global-search/route.js.map +7 -0
- package/dist/modules/search/api/settings/route.js +118 -0
- package/dist/modules/search/api/settings/route.js.map +7 -0
- package/dist/modules/search/api/settings/vector-store/route.js +77 -0
- package/dist/modules/search/api/settings/vector-store/route.js.map +7 -0
- package/dist/modules/search/backend/config/search/page.js +10 -0
- package/dist/modules/search/backend/config/search/page.js.map +7 -0
- package/dist/modules/search/backend/config/search/page.meta.js +24 -0
- package/dist/modules/search/backend/config/search/page.meta.js.map +7 -0
- package/dist/modules/search/cli.js +698 -0
- package/dist/modules/search/cli.js.map +7 -0
- package/dist/modules/search/di.js +32 -0
- package/dist/modules/search/di.js.map +7 -0
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js +357 -0
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js.map +7 -0
- package/dist/modules/search/frontend/components/HybridSearchTable.js +343 -0
- package/dist/modules/search/frontend/components/HybridSearchTable.js.map +7 -0
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +303 -0
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +360 -0
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js +101 -0
- package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +608 -0
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/index.js +9 -0
- package/dist/modules/search/frontend/index.js.map +7 -0
- package/dist/modules/search/frontend/utils.js +41 -0
- package/dist/modules/search/frontend/utils.js.map +7 -0
- package/dist/modules/search/i18n/de.json +61 -0
- package/dist/modules/search/i18n/en.json +72 -0
- package/dist/modules/search/i18n/es.json +61 -0
- package/dist/modules/search/i18n/pl.json +61 -0
- package/dist/modules/search/index.js +11 -0
- package/dist/modules/search/index.js.map +7 -0
- package/dist/modules/search/lib/auto-indexing.js +29 -0
- package/dist/modules/search/lib/auto-indexing.js.map +7 -0
- package/dist/modules/search/lib/embedding-config.js +131 -0
- package/dist/modules/search/lib/embedding-config.js.map +7 -0
- package/dist/modules/search/lib/global-search-config.js +45 -0
- package/dist/modules/search/lib/global-search-config.js.map +7 -0
- package/dist/modules/search/lib/reindex-lock.js +99 -0
- package/dist/modules/search/lib/reindex-lock.js.map +7 -0
- package/dist/modules/search/subscribers/fulltext_upsert.js +64 -0
- package/dist/modules/search/subscribers/fulltext_upsert.js.map +7 -0
- package/dist/modules/search/subscribers/vector_delete.js +58 -0
- package/dist/modules/search/subscribers/vector_delete.js.map +7 -0
- package/dist/modules/search/subscribers/vector_purge.js +142 -0
- package/dist/modules/search/subscribers/vector_purge.js.map +7 -0
- package/dist/modules/search/subscribers/vector_upsert.js +58 -0
- package/dist/modules/search/subscribers/vector_upsert.js.map +7 -0
- package/dist/modules/search/workers/fulltext-index.worker.js +240 -0
- package/dist/modules/search/workers/fulltext-index.worker.js.map +7 -0
- package/dist/modules/search/workers/vector-index.worker.js +234 -0
- package/dist/modules/search/workers/vector-index.worker.js.map +7 -0
- package/dist/queue/fulltext-indexing.js +15 -0
- package/dist/queue/fulltext-indexing.js.map +7 -0
- package/dist/queue/index.js +3 -0
- package/dist/queue/index.js.map +7 -0
- package/dist/queue/vector-indexing.js +15 -0
- package/dist/queue/vector-indexing.js.map +7 -0
- package/dist/service.js +286 -0
- package/dist/service.js.map +7 -0
- package/dist/strategies/fulltext.strategy.js +116 -0
- package/dist/strategies/fulltext.strategy.js.map +7 -0
- package/dist/strategies/index.js +12 -0
- package/dist/strategies/index.js.map +7 -0
- package/dist/strategies/token.strategy.js +80 -0
- package/dist/strategies/token.strategy.js.map +7 -0
- package/dist/strategies/vector.strategy.js +137 -0
- package/dist/strategies/vector.strategy.js.map +7 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +7 -0
- package/dist/vector/drivers/chromadb/index.js +44 -0
- package/dist/vector/drivers/chromadb/index.js.map +7 -0
- package/dist/vector/drivers/index.js +9 -0
- package/dist/vector/drivers/index.js.map +7 -0
- package/dist/vector/drivers/pgvector/index.js +509 -0
- package/dist/vector/drivers/pgvector/index.js.map +7 -0
- package/dist/vector/drivers/qdrant/index.js +44 -0
- package/dist/vector/drivers/qdrant/index.js.map +7 -0
- package/dist/vector/index.js +4 -0
- package/dist/vector/index.js.map +7 -0
- package/dist/vector/lib/vector-logs.js +33 -0
- package/dist/vector/lib/vector-logs.js.map +7 -0
- package/dist/vector/services/checksum.js +20 -0
- package/dist/vector/services/checksum.js.map +7 -0
- package/dist/vector/services/embedding.js +222 -0
- package/dist/vector/services/embedding.js.map +7 -0
- package/dist/vector/services/index.js +4 -0
- package/dist/vector/services/index.js.map +7 -0
- package/dist/vector/services/vector-index.service.js +960 -0
- package/dist/vector/services/vector-index.service.js.map +7 -0
- package/dist/vector/types/pg.d.js +1 -0
- package/dist/vector/types/pg.d.js.map +7 -0
- package/dist/vector/types.js +75 -0
- package/dist/vector/types.js.map +7 -0
- package/jest.config.cjs +19 -0
- package/package.json +142 -0
- package/src/__tests__/queue.test.ts +148 -0
- package/src/__tests__/service.test.ts +345 -0
- package/src/__tests__/workers.test.ts +319 -0
- package/src/di.ts +291 -0
- package/src/fulltext/drivers/index.ts +41 -0
- package/src/fulltext/drivers/meilisearch/index.ts +410 -0
- package/src/fulltext/index.ts +13 -0
- package/src/fulltext/types.ts +115 -0
- package/src/index.ts +36 -0
- package/src/indexer/index.ts +13 -0
- package/src/indexer/search-indexer.ts +1141 -0
- package/src/indexer/subscribers/delete.ts +49 -0
- package/src/lib/debug.ts +46 -0
- package/src/lib/fallback-presenter.ts +106 -0
- package/src/lib/field-policy.ts +169 -0
- package/src/lib/index.ts +13 -0
- package/src/lib/merger.ts +159 -0
- package/src/lib/presenter-enricher.ts +323 -0
- package/src/modules/search/README.md +694 -0
- package/src/modules/search/acl.ts +10 -0
- package/src/modules/search/ai-tools.ts +467 -0
- package/src/modules/search/api/embeddings/reindex/cancel/route.ts +77 -0
- package/src/modules/search/api/embeddings/reindex/route.ts +197 -0
- package/src/modules/search/api/embeddings/route.ts +304 -0
- package/src/modules/search/api/index/route.ts +297 -0
- package/src/modules/search/api/reindex/cancel/route.ts +77 -0
- package/src/modules/search/api/reindex/route.ts +419 -0
- package/src/modules/search/api/search/global/route.ts +120 -0
- package/src/modules/search/api/search/route.ts +121 -0
- package/src/modules/search/api/settings/fulltext/route.ts +82 -0
- package/src/modules/search/api/settings/global-search/route.ts +91 -0
- package/src/modules/search/api/settings/route.ts +187 -0
- package/src/modules/search/api/settings/vector-store/route.ts +105 -0
- package/src/modules/search/backend/config/search/page.meta.ts +22 -0
- package/src/modules/search/backend/config/search/page.tsx +12 -0
- package/src/modules/search/cli.ts +818 -0
- package/src/modules/search/di.ts +50 -0
- package/src/modules/search/frontend/components/GlobalSearchDialog.tsx +436 -0
- package/src/modules/search/frontend/components/HybridSearchTable.tsx +418 -0
- package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +476 -0
- package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +624 -0
- package/src/modules/search/frontend/components/sections/GlobalSearchSection.tsx +124 -0
- package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +943 -0
- package/src/modules/search/frontend/index.ts +3 -0
- package/src/modules/search/frontend/utils.ts +82 -0
- package/src/modules/search/i18n/de.json +61 -0
- package/src/modules/search/i18n/en.json +72 -0
- package/src/modules/search/i18n/es.json +61 -0
- package/src/modules/search/i18n/pl.json +61 -0
- package/src/modules/search/index.ts +9 -0
- package/src/modules/search/lib/auto-indexing.ts +35 -0
- package/src/modules/search/lib/embedding-config.ts +161 -0
- package/src/modules/search/lib/global-search-config.ts +69 -0
- package/src/modules/search/lib/reindex-lock.ts +201 -0
- package/src/modules/search/subscribers/fulltext_upsert.ts +83 -0
- package/src/modules/search/subscribers/vector_delete.ts +75 -0
- package/src/modules/search/subscribers/vector_purge.ts +161 -0
- package/src/modules/search/subscribers/vector_upsert.ts +75 -0
- package/src/modules/search/workers/fulltext-index.worker.ts +318 -0
- package/src/modules/search/workers/vector-index.worker.ts +292 -0
- package/src/queue/fulltext-indexing.ts +87 -0
- package/src/queue/index.ts +2 -0
- package/src/queue/vector-indexing.ts +66 -0
- package/src/service.ts +397 -0
- package/src/strategies/fulltext.strategy.ts +155 -0
- package/src/strategies/index.ts +17 -0
- package/src/strategies/token.strategy.ts +153 -0
- package/src/strategies/vector.strategy.ts +234 -0
- package/src/types.ts +38 -0
- package/src/vector/drivers/chromadb/index.ts +49 -0
- package/src/vector/drivers/index.ts +4 -0
- package/src/vector/drivers/pgvector/index.ts +627 -0
- package/src/vector/drivers/qdrant/index.ts +49 -0
- package/src/vector/index.ts +3 -0
- package/src/vector/lib/vector-logs.ts +46 -0
- package/src/vector/services/checksum.ts +18 -0
- package/src/vector/services/embedding.ts +275 -0
- package/src/vector/services/index.ts +3 -0
- package/src/vector/services/vector-index.service.ts +1234 -0
- package/src/vector/types/pg.d.ts +1 -0
- package/src/vector/types.ts +220 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { SearchService } from '../service'
|
|
2
|
+
import { SearchStrategy, SearchResult, IndexableRecord } from '../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a mock search strategy for testing
|
|
6
|
+
*/
|
|
7
|
+
function createMockStrategy(overrides: Partial<SearchStrategy> = {}): SearchStrategy {
|
|
8
|
+
return {
|
|
9
|
+
id: 'mock',
|
|
10
|
+
name: 'Mock Strategy',
|
|
11
|
+
priority: 10,
|
|
12
|
+
isAvailable: jest.fn().mockResolvedValue(true),
|
|
13
|
+
ensureReady: jest.fn().mockResolvedValue(undefined),
|
|
14
|
+
search: jest.fn().mockResolvedValue([]),
|
|
15
|
+
index: jest.fn().mockResolvedValue(undefined),
|
|
16
|
+
delete: jest.fn().mockResolvedValue(undefined),
|
|
17
|
+
bulkIndex: jest.fn().mockResolvedValue(undefined),
|
|
18
|
+
purge: jest.fn().mockResolvedValue(undefined),
|
|
19
|
+
...overrides,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a mock search result for testing
|
|
25
|
+
*/
|
|
26
|
+
function createMockResult(overrides: Partial<SearchResult> = {}): SearchResult {
|
|
27
|
+
return {
|
|
28
|
+
entityId: 'test:entity',
|
|
29
|
+
recordId: 'rec-123',
|
|
30
|
+
score: 0.9,
|
|
31
|
+
source: 'mock',
|
|
32
|
+
presenter: { title: 'Test Result' },
|
|
33
|
+
...overrides,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a mock indexable record for testing
|
|
39
|
+
*/
|
|
40
|
+
function createMockRecord(overrides: Partial<IndexableRecord> = {}): IndexableRecord {
|
|
41
|
+
return {
|
|
42
|
+
entityId: 'test:entity',
|
|
43
|
+
recordId: 'rec-123',
|
|
44
|
+
tenantId: 'tenant-123',
|
|
45
|
+
organizationId: 'org-456',
|
|
46
|
+
fields: { name: 'Test' },
|
|
47
|
+
presenter: { title: 'Test Record' },
|
|
48
|
+
...overrides,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('SearchService', () => {
|
|
53
|
+
describe('constructor', () => {
|
|
54
|
+
it('should create service with no strategies', () => {
|
|
55
|
+
const service = new SearchService()
|
|
56
|
+
expect(service.getRegisteredStrategies()).toEqual([])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should create service with provided strategies', () => {
|
|
60
|
+
const strategy = createMockStrategy({ id: 'test' })
|
|
61
|
+
const service = new SearchService({ strategies: [strategy] })
|
|
62
|
+
expect(service.getRegisteredStrategies()).toContain('test')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should use default strategies when not specified', () => {
|
|
66
|
+
const service = new SearchService()
|
|
67
|
+
expect(service.getDefaultStrategies()).toEqual(['tokens'])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should use custom default strategies when provided', () => {
|
|
71
|
+
const service = new SearchService({
|
|
72
|
+
defaultStrategies: ['meilisearch', 'vector'],
|
|
73
|
+
})
|
|
74
|
+
expect(service.getDefaultStrategies()).toEqual(['meilisearch', 'vector'])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('strategy management', () => {
|
|
79
|
+
it('should register a strategy', () => {
|
|
80
|
+
const service = new SearchService()
|
|
81
|
+
const strategy = createMockStrategy({ id: 'new-strategy' })
|
|
82
|
+
|
|
83
|
+
service.registerStrategy(strategy)
|
|
84
|
+
|
|
85
|
+
expect(service.getRegisteredStrategies()).toContain('new-strategy')
|
|
86
|
+
expect(service.getStrategy('new-strategy')).toBe(strategy)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should unregister a strategy', () => {
|
|
90
|
+
const strategy = createMockStrategy({ id: 'test' })
|
|
91
|
+
const service = new SearchService({ strategies: [strategy] })
|
|
92
|
+
|
|
93
|
+
service.unregisterStrategy('test')
|
|
94
|
+
|
|
95
|
+
expect(service.getRegisteredStrategies()).not.toContain('test')
|
|
96
|
+
expect(service.getStrategy('test')).toBeUndefined()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should check strategy availability', async () => {
|
|
100
|
+
const availableStrategy = createMockStrategy({
|
|
101
|
+
id: 'available',
|
|
102
|
+
isAvailable: jest.fn().mockResolvedValue(true),
|
|
103
|
+
})
|
|
104
|
+
const unavailableStrategy = createMockStrategy({
|
|
105
|
+
id: 'unavailable',
|
|
106
|
+
isAvailable: jest.fn().mockResolvedValue(false),
|
|
107
|
+
})
|
|
108
|
+
const service = new SearchService({
|
|
109
|
+
strategies: [availableStrategy, unavailableStrategy],
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
expect(await service.isStrategyAvailable('available')).toBe(true)
|
|
113
|
+
expect(await service.isStrategyAvailable('unavailable')).toBe(false)
|
|
114
|
+
expect(await service.isStrategyAvailable('nonexistent')).toBe(false)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('search', () => {
|
|
119
|
+
it('should return empty array when no strategies available', async () => {
|
|
120
|
+
const service = new SearchService()
|
|
121
|
+
|
|
122
|
+
const results = await service.search('test query', { tenantId: 'tenant-123' })
|
|
123
|
+
|
|
124
|
+
expect(results).toEqual([])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should execute search on available strategies', async () => {
|
|
128
|
+
const mockResults = [createMockResult()]
|
|
129
|
+
const strategy = createMockStrategy({
|
|
130
|
+
id: 'test',
|
|
131
|
+
search: jest.fn().mockResolvedValue(mockResults),
|
|
132
|
+
})
|
|
133
|
+
const service = new SearchService({
|
|
134
|
+
strategies: [strategy],
|
|
135
|
+
defaultStrategies: ['test'],
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const results = await service.search('test query', { tenantId: 'tenant-123' })
|
|
139
|
+
|
|
140
|
+
expect(strategy.ensureReady).toHaveBeenCalled()
|
|
141
|
+
expect(strategy.search).toHaveBeenCalledWith('test query', { tenantId: 'tenant-123' })
|
|
142
|
+
expect(results).toHaveLength(1)
|
|
143
|
+
expect(results[0].recordId).toBe('rec-123')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should skip unavailable strategies', async () => {
|
|
147
|
+
const availableStrategy = createMockStrategy({
|
|
148
|
+
id: 'available',
|
|
149
|
+
isAvailable: jest.fn().mockResolvedValue(true),
|
|
150
|
+
search: jest.fn().mockResolvedValue([createMockResult({ source: 'available' })]),
|
|
151
|
+
})
|
|
152
|
+
const unavailableStrategy = createMockStrategy({
|
|
153
|
+
id: 'unavailable',
|
|
154
|
+
isAvailable: jest.fn().mockResolvedValue(false),
|
|
155
|
+
search: jest.fn().mockResolvedValue([createMockResult({ source: 'unavailable' })]),
|
|
156
|
+
})
|
|
157
|
+
const service = new SearchService({
|
|
158
|
+
strategies: [availableStrategy, unavailableStrategy],
|
|
159
|
+
defaultStrategies: ['available', 'unavailable'],
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const results = await service.search('test', { tenantId: 'tenant-123' })
|
|
163
|
+
|
|
164
|
+
expect(unavailableStrategy.search).not.toHaveBeenCalled()
|
|
165
|
+
expect(results.every((r) => r.source === 'available')).toBe(true)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should merge results from multiple strategies', async () => {
|
|
169
|
+
const strategy1 = createMockStrategy({
|
|
170
|
+
id: 'strategy1',
|
|
171
|
+
priority: 20,
|
|
172
|
+
search: jest.fn().mockResolvedValue([
|
|
173
|
+
createMockResult({ recordId: 'rec-1', source: 'strategy1', score: 0.9 }),
|
|
174
|
+
]),
|
|
175
|
+
})
|
|
176
|
+
const strategy2 = createMockStrategy({
|
|
177
|
+
id: 'strategy2',
|
|
178
|
+
priority: 10,
|
|
179
|
+
search: jest.fn().mockResolvedValue([
|
|
180
|
+
createMockResult({ recordId: 'rec-2', source: 'strategy2', score: 0.8 }),
|
|
181
|
+
]),
|
|
182
|
+
})
|
|
183
|
+
const service = new SearchService({
|
|
184
|
+
strategies: [strategy1, strategy2],
|
|
185
|
+
defaultStrategies: ['strategy1', 'strategy2'],
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const results = await service.search('test', { tenantId: 'tenant-123' })
|
|
189
|
+
|
|
190
|
+
expect(results).toHaveLength(2)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should handle strategy search failures gracefully', async () => {
|
|
194
|
+
const failingStrategy = createMockStrategy({
|
|
195
|
+
id: 'failing',
|
|
196
|
+
search: jest.fn().mockRejectedValue(new Error('Search failed')),
|
|
197
|
+
})
|
|
198
|
+
const workingStrategy = createMockStrategy({
|
|
199
|
+
id: 'working',
|
|
200
|
+
search: jest.fn().mockResolvedValue([createMockResult({ source: 'working' })]),
|
|
201
|
+
})
|
|
202
|
+
const service = new SearchService({
|
|
203
|
+
strategies: [failingStrategy, workingStrategy],
|
|
204
|
+
defaultStrategies: ['failing', 'working'],
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const results = await service.search('test', { tenantId: 'tenant-123' })
|
|
208
|
+
|
|
209
|
+
// Should return results from working strategy despite failing strategy
|
|
210
|
+
expect(results).toHaveLength(1)
|
|
211
|
+
expect(results[0].source).toBe('working')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should use fallback strategy when no default strategies available', async () => {
|
|
215
|
+
const fallbackStrategy = createMockStrategy({
|
|
216
|
+
id: 'fallback',
|
|
217
|
+
search: jest.fn().mockResolvedValue([createMockResult({ source: 'fallback' })]),
|
|
218
|
+
})
|
|
219
|
+
const unavailableStrategy = createMockStrategy({
|
|
220
|
+
id: 'primary',
|
|
221
|
+
isAvailable: jest.fn().mockResolvedValue(false),
|
|
222
|
+
})
|
|
223
|
+
const service = new SearchService({
|
|
224
|
+
strategies: [unavailableStrategy, fallbackStrategy],
|
|
225
|
+
defaultStrategies: ['primary'],
|
|
226
|
+
fallbackStrategy: 'fallback',
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const results = await service.search('test', { tenantId: 'tenant-123' })
|
|
230
|
+
|
|
231
|
+
expect(results).toHaveLength(1)
|
|
232
|
+
expect(results[0].source).toBe('fallback')
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('index', () => {
|
|
237
|
+
it('should index record on all available strategies', async () => {
|
|
238
|
+
const strategy1 = createMockStrategy({ id: 'strategy1' })
|
|
239
|
+
const strategy2 = createMockStrategy({ id: 'strategy2' })
|
|
240
|
+
const service = new SearchService({
|
|
241
|
+
strategies: [strategy1, strategy2],
|
|
242
|
+
})
|
|
243
|
+
const record = createMockRecord()
|
|
244
|
+
|
|
245
|
+
await service.index(record)
|
|
246
|
+
|
|
247
|
+
expect(strategy1.index).toHaveBeenCalledWith(record)
|
|
248
|
+
expect(strategy2.index).toHaveBeenCalledWith(record)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should skip unavailable strategies', async () => {
|
|
252
|
+
const availableStrategy = createMockStrategy({
|
|
253
|
+
id: 'available',
|
|
254
|
+
isAvailable: jest.fn().mockResolvedValue(true),
|
|
255
|
+
})
|
|
256
|
+
const unavailableStrategy = createMockStrategy({
|
|
257
|
+
id: 'unavailable',
|
|
258
|
+
isAvailable: jest.fn().mockResolvedValue(false),
|
|
259
|
+
})
|
|
260
|
+
const service = new SearchService({
|
|
261
|
+
strategies: [availableStrategy, unavailableStrategy],
|
|
262
|
+
})
|
|
263
|
+
const record = createMockRecord()
|
|
264
|
+
|
|
265
|
+
await service.index(record)
|
|
266
|
+
|
|
267
|
+
expect(availableStrategy.index).toHaveBeenCalled()
|
|
268
|
+
expect(unavailableStrategy.index).not.toHaveBeenCalled()
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('bulkIndex', () => {
|
|
273
|
+
it('should bulk index records on strategies that support it', async () => {
|
|
274
|
+
const strategy = createMockStrategy({
|
|
275
|
+
id: 'test',
|
|
276
|
+
bulkIndex: jest.fn().mockResolvedValue(undefined),
|
|
277
|
+
})
|
|
278
|
+
const service = new SearchService({ strategies: [strategy] })
|
|
279
|
+
const records = [createMockRecord({ recordId: 'rec-1' }), createMockRecord({ recordId: 'rec-2' })]
|
|
280
|
+
|
|
281
|
+
await service.bulkIndex(records)
|
|
282
|
+
|
|
283
|
+
expect(strategy.bulkIndex).toHaveBeenCalledWith(records)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('should fallback to individual indexing when bulkIndex not supported', async () => {
|
|
287
|
+
const strategy = createMockStrategy({
|
|
288
|
+
id: 'test',
|
|
289
|
+
bulkIndex: undefined,
|
|
290
|
+
index: jest.fn().mockResolvedValue(undefined),
|
|
291
|
+
})
|
|
292
|
+
const service = new SearchService({ strategies: [strategy] })
|
|
293
|
+
const records = [createMockRecord({ recordId: 'rec-1' }), createMockRecord({ recordId: 'rec-2' })]
|
|
294
|
+
|
|
295
|
+
await service.bulkIndex(records)
|
|
296
|
+
|
|
297
|
+
expect(strategy.index).toHaveBeenCalledTimes(2)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should do nothing when records array is empty', async () => {
|
|
301
|
+
const strategy = createMockStrategy({ id: 'test' })
|
|
302
|
+
const service = new SearchService({ strategies: [strategy] })
|
|
303
|
+
|
|
304
|
+
await service.bulkIndex([])
|
|
305
|
+
|
|
306
|
+
expect(strategy.bulkIndex).not.toHaveBeenCalled()
|
|
307
|
+
expect(strategy.index).not.toHaveBeenCalled()
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
describe('delete', () => {
|
|
312
|
+
it('should delete from all available strategies', async () => {
|
|
313
|
+
const strategy1 = createMockStrategy({ id: 'strategy1' })
|
|
314
|
+
const strategy2 = createMockStrategy({ id: 'strategy2' })
|
|
315
|
+
const service = new SearchService({
|
|
316
|
+
strategies: [strategy1, strategy2],
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
await service.delete('test:entity', 'rec-123', 'tenant-123')
|
|
320
|
+
|
|
321
|
+
expect(strategy1.delete).toHaveBeenCalledWith('test:entity', 'rec-123', 'tenant-123')
|
|
322
|
+
expect(strategy2.delete).toHaveBeenCalledWith('test:entity', 'rec-123', 'tenant-123')
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
describe('purge', () => {
|
|
327
|
+
it('should purge from strategies that support it', async () => {
|
|
328
|
+
const strategyWithPurge = createMockStrategy({
|
|
329
|
+
id: 'with-purge',
|
|
330
|
+
purge: jest.fn().mockResolvedValue(undefined),
|
|
331
|
+
})
|
|
332
|
+
const strategyWithoutPurge = createMockStrategy({
|
|
333
|
+
id: 'without-purge',
|
|
334
|
+
purge: undefined,
|
|
335
|
+
})
|
|
336
|
+
const service = new SearchService({
|
|
337
|
+
strategies: [strategyWithPurge, strategyWithoutPurge],
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
await service.purge('test:entity', 'tenant-123')
|
|
341
|
+
|
|
342
|
+
expect(strategyWithPurge.purge).toHaveBeenCalledWith('test:entity', 'tenant-123')
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
})
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { QueuedJob, JobContext } from '@open-mercato/queue'
|
|
2
|
+
import { VectorIndexJobPayload } from '../queue/vector-indexing'
|
|
3
|
+
import { FulltextIndexJobPayload } from '../queue/fulltext-indexing'
|
|
4
|
+
|
|
5
|
+
type HandlerContext = { resolve: <T = unknown>(name: string) => T }
|
|
6
|
+
|
|
7
|
+
// Mock dependencies before importing workers
|
|
8
|
+
jest.mock('@open-mercato/shared/lib/indexers/error-log', () => ({
|
|
9
|
+
recordIndexerError: jest.fn().mockResolvedValue(undefined),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
jest.mock('@open-mercato/shared/lib/indexers/status-log', () => ({
|
|
13
|
+
recordIndexerLog: jest.fn().mockResolvedValue(undefined),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
jest.mock('@open-mercato/core/modules/query_index/lib/coverage', () => ({
|
|
17
|
+
applyCoverageAdjustments: jest.fn().mockResolvedValue(undefined),
|
|
18
|
+
createCoverageAdjustments: jest.fn().mockReturnValue([]),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
jest.mock('../vector/lib/vector-logs', () => ({
|
|
22
|
+
logVectorOperation: jest.fn().mockResolvedValue(undefined),
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
jest.mock('../modules/search/lib/auto-indexing', () => ({
|
|
26
|
+
resolveAutoIndexingEnabled: jest.fn().mockResolvedValue(true),
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
jest.mock('../modules/search/lib/embedding-config', () => ({
|
|
30
|
+
resolveEmbeddingConfig: jest.fn().mockResolvedValue(null),
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
import { handleVectorIndexJob } from '../modules/search/workers/vector-index.worker'
|
|
34
|
+
import { handleFulltextIndexJob } from '../modules/search/workers/fulltext-index.worker'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a mock job context
|
|
38
|
+
*/
|
|
39
|
+
function createMockJobContext(overrides: Partial<JobContext> = {}): JobContext {
|
|
40
|
+
return {
|
|
41
|
+
jobId: 'job-123',
|
|
42
|
+
attemptNumber: 1,
|
|
43
|
+
queueName: 'test-queue',
|
|
44
|
+
...overrides,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a mock queued job
|
|
50
|
+
*/
|
|
51
|
+
function createMockJob<T>(payload: T): QueuedJob<T> {
|
|
52
|
+
return {
|
|
53
|
+
id: 'job-123',
|
|
54
|
+
payload,
|
|
55
|
+
createdAt: new Date().toISOString(),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('Vector Index Worker', () => {
|
|
60
|
+
const mockSearchIndexer = {
|
|
61
|
+
indexRecordById: jest.fn().mockResolvedValue({ action: 'indexed', created: true }),
|
|
62
|
+
deleteRecord: jest.fn().mockResolvedValue({ action: 'deleted', existed: true }),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mockContainer: HandlerContext = {
|
|
66
|
+
resolve: jest.fn((name: string) => {
|
|
67
|
+
if (name === 'searchIndexer') return mockSearchIndexer
|
|
68
|
+
if (name === 'em') return null
|
|
69
|
+
if (name === 'eventBus') return null
|
|
70
|
+
if (name === 'vectorEmbeddingService') return { updateConfig: jest.fn() }
|
|
71
|
+
throw new Error(`Unknown service: ${name}`)
|
|
72
|
+
}) as HandlerContext['resolve'],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
jest.clearAllMocks()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should skip job with missing required fields', async () => {
|
|
80
|
+
const job = createMockJob<VectorIndexJobPayload>({
|
|
81
|
+
jobType: 'index',
|
|
82
|
+
entityType: '',
|
|
83
|
+
recordId: 'rec-123',
|
|
84
|
+
tenantId: 'tenant-123',
|
|
85
|
+
organizationId: null,
|
|
86
|
+
})
|
|
87
|
+
const ctx = createMockJobContext()
|
|
88
|
+
|
|
89
|
+
await handleVectorIndexJob(job, ctx, mockContainer)
|
|
90
|
+
|
|
91
|
+
expect(mockSearchIndexer.indexRecordById).not.toHaveBeenCalled()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should index record when jobType is index', async () => {
|
|
95
|
+
const job = createMockJob<VectorIndexJobPayload>({
|
|
96
|
+
jobType: 'index',
|
|
97
|
+
entityType: 'customers:customer_person_profile',
|
|
98
|
+
recordId: 'rec-123',
|
|
99
|
+
tenantId: 'tenant-123',
|
|
100
|
+
organizationId: 'org-456',
|
|
101
|
+
})
|
|
102
|
+
const ctx = createMockJobContext()
|
|
103
|
+
|
|
104
|
+
await handleVectorIndexJob(job, ctx, mockContainer)
|
|
105
|
+
|
|
106
|
+
expect(mockSearchIndexer.indexRecordById).toHaveBeenCalledWith({
|
|
107
|
+
entityId: 'customers:customer_person_profile',
|
|
108
|
+
recordId: 'rec-123',
|
|
109
|
+
tenantId: 'tenant-123',
|
|
110
|
+
organizationId: 'org-456',
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should delete record when jobType is delete', async () => {
|
|
115
|
+
const job = createMockJob<VectorIndexJobPayload>({
|
|
116
|
+
jobType: 'delete',
|
|
117
|
+
entityType: 'customers:customer_person_profile',
|
|
118
|
+
recordId: 'rec-123',
|
|
119
|
+
tenantId: 'tenant-123',
|
|
120
|
+
organizationId: null,
|
|
121
|
+
})
|
|
122
|
+
const ctx = createMockJobContext()
|
|
123
|
+
|
|
124
|
+
await handleVectorIndexJob(job, ctx, mockContainer)
|
|
125
|
+
|
|
126
|
+
expect(mockSearchIndexer.deleteRecord).toHaveBeenCalledWith({
|
|
127
|
+
entityId: 'customers:customer_person_profile',
|
|
128
|
+
recordId: 'rec-123',
|
|
129
|
+
tenantId: 'tenant-123',
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should skip when vectorIndexService is not available', async () => {
|
|
134
|
+
const containerWithoutService: HandlerContext = {
|
|
135
|
+
resolve: jest.fn(() => {
|
|
136
|
+
throw new Error('Service not available')
|
|
137
|
+
}) as HandlerContext['resolve'],
|
|
138
|
+
}
|
|
139
|
+
const job = createMockJob<VectorIndexJobPayload>({
|
|
140
|
+
jobType: 'index',
|
|
141
|
+
entityType: 'test:entity',
|
|
142
|
+
recordId: 'rec-123',
|
|
143
|
+
tenantId: 'tenant-123',
|
|
144
|
+
organizationId: null,
|
|
145
|
+
})
|
|
146
|
+
const ctx = createMockJobContext()
|
|
147
|
+
|
|
148
|
+
// Should not throw
|
|
149
|
+
await handleVectorIndexJob(job, ctx, containerWithoutService)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('Fulltext Index Worker', () => {
|
|
154
|
+
const mockFulltextStrategy = {
|
|
155
|
+
id: 'fulltext',
|
|
156
|
+
isAvailable: jest.fn().mockResolvedValue(true),
|
|
157
|
+
bulkIndex: jest.fn().mockResolvedValue(undefined),
|
|
158
|
+
delete: jest.fn().mockResolvedValue(undefined),
|
|
159
|
+
purge: jest.fn().mockResolvedValue(undefined),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Mock knex query builder for batch-index tests
|
|
163
|
+
const mockKnexQuery = {
|
|
164
|
+
select: jest.fn().mockReturnThis(),
|
|
165
|
+
where: jest.fn().mockReturnThis(),
|
|
166
|
+
whereIn: jest.fn().mockReturnThis(),
|
|
167
|
+
whereNull: jest.fn().mockResolvedValue([
|
|
168
|
+
{ entity_id: 'rec-1', doc: { name: 'Test 1' } },
|
|
169
|
+
{ entity_id: 'rec-2', doc: { name: 'Test 2' } },
|
|
170
|
+
]),
|
|
171
|
+
}
|
|
172
|
+
const mockKnex = jest.fn(() => mockKnexQuery)
|
|
173
|
+
|
|
174
|
+
const mockSearchIndexer = {
|
|
175
|
+
getEntityConfig: jest.fn().mockReturnValue(null),
|
|
176
|
+
indexRecordById: jest.fn().mockResolvedValue({ action: 'indexed', created: true }),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const mockEm = {
|
|
180
|
+
getConnection: jest.fn().mockReturnValue({
|
|
181
|
+
getKnex: jest.fn().mockReturnValue(mockKnex),
|
|
182
|
+
}),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const mockContainer: HandlerContext = {
|
|
186
|
+
resolve: jest.fn((name: string) => {
|
|
187
|
+
if (name === 'searchStrategies') return [mockFulltextStrategy]
|
|
188
|
+
if (name === 'em') return mockEm
|
|
189
|
+
if (name === 'searchIndexer') return mockSearchIndexer
|
|
190
|
+
throw new Error(`Unknown service: ${name}`)
|
|
191
|
+
}) as HandlerContext['resolve'],
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
beforeEach(() => {
|
|
195
|
+
jest.clearAllMocks()
|
|
196
|
+
// Reset knex mock chain
|
|
197
|
+
mockKnexQuery.select.mockReturnThis()
|
|
198
|
+
mockKnexQuery.where.mockReturnThis()
|
|
199
|
+
mockKnexQuery.whereIn.mockReturnThis()
|
|
200
|
+
mockKnexQuery.whereNull.mockResolvedValue([
|
|
201
|
+
{ entity_id: 'rec-1', doc: { name: 'Test 1' } },
|
|
202
|
+
{ entity_id: 'rec-2', doc: { name: 'Test 2' } },
|
|
203
|
+
])
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should skip job with missing tenantId', async () => {
|
|
207
|
+
const job = createMockJob<FulltextIndexJobPayload>({
|
|
208
|
+
jobType: 'batch-index',
|
|
209
|
+
tenantId: '',
|
|
210
|
+
records: [],
|
|
211
|
+
})
|
|
212
|
+
const ctx = createMockJobContext()
|
|
213
|
+
|
|
214
|
+
await handleFulltextIndexJob(job, ctx, mockContainer)
|
|
215
|
+
|
|
216
|
+
expect(mockFulltextStrategy.bulkIndex).not.toHaveBeenCalled()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should index records via searchIndexer when jobType is batch-index', async () => {
|
|
220
|
+
// Use minimal record format (just entityId + recordId)
|
|
221
|
+
const records = [
|
|
222
|
+
{ entityId: 'test:entity', recordId: 'rec-1' },
|
|
223
|
+
{ entityId: 'test:entity', recordId: 'rec-2' },
|
|
224
|
+
]
|
|
225
|
+
const job = createMockJob<FulltextIndexJobPayload>({
|
|
226
|
+
jobType: 'batch-index',
|
|
227
|
+
tenantId: 'tenant-123',
|
|
228
|
+
records,
|
|
229
|
+
})
|
|
230
|
+
const ctx = createMockJobContext()
|
|
231
|
+
|
|
232
|
+
await handleFulltextIndexJob(job, ctx, mockContainer)
|
|
233
|
+
|
|
234
|
+
// Verify indexRecordById was called for each record
|
|
235
|
+
expect(mockSearchIndexer.indexRecordById).toHaveBeenCalledTimes(2)
|
|
236
|
+
expect(mockSearchIndexer.indexRecordById).toHaveBeenCalledWith({
|
|
237
|
+
entityId: 'test:entity',
|
|
238
|
+
recordId: 'rec-1',
|
|
239
|
+
tenantId: 'tenant-123',
|
|
240
|
+
organizationId: undefined,
|
|
241
|
+
})
|
|
242
|
+
expect(mockSearchIndexer.indexRecordById).toHaveBeenCalledWith({
|
|
243
|
+
entityId: 'test:entity',
|
|
244
|
+
recordId: 'rec-2',
|
|
245
|
+
tenantId: 'tenant-123',
|
|
246
|
+
organizationId: undefined,
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should skip batch-index with empty records', async () => {
|
|
251
|
+
const job = createMockJob<FulltextIndexJobPayload>({
|
|
252
|
+
jobType: 'batch-index',
|
|
253
|
+
tenantId: 'tenant-123',
|
|
254
|
+
records: [],
|
|
255
|
+
})
|
|
256
|
+
const ctx = createMockJobContext()
|
|
257
|
+
|
|
258
|
+
await handleFulltextIndexJob(job, ctx, mockContainer)
|
|
259
|
+
|
|
260
|
+
expect(mockFulltextStrategy.bulkIndex).not.toHaveBeenCalled()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should delete record when jobType is delete', async () => {
|
|
264
|
+
const job = createMockJob<FulltextIndexJobPayload>({
|
|
265
|
+
jobType: 'delete',
|
|
266
|
+
tenantId: 'tenant-123',
|
|
267
|
+
entityId: 'test:entity',
|
|
268
|
+
recordId: 'rec-123',
|
|
269
|
+
})
|
|
270
|
+
const ctx = createMockJobContext()
|
|
271
|
+
|
|
272
|
+
await handleFulltextIndexJob(job, ctx, mockContainer)
|
|
273
|
+
|
|
274
|
+
expect(mockFulltextStrategy.delete).toHaveBeenCalledWith('test:entity', 'rec-123', 'tenant-123')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should purge entity when jobType is purge', async () => {
|
|
278
|
+
const job = createMockJob<FulltextIndexJobPayload>({
|
|
279
|
+
jobType: 'purge',
|
|
280
|
+
tenantId: 'tenant-123',
|
|
281
|
+
entityId: 'test:entity',
|
|
282
|
+
})
|
|
283
|
+
const ctx = createMockJobContext()
|
|
284
|
+
|
|
285
|
+
await handleFulltextIndexJob(job, ctx, mockContainer)
|
|
286
|
+
|
|
287
|
+
expect(mockFulltextStrategy.purge).toHaveBeenCalledWith('test:entity', 'tenant-123')
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should skip when fulltext strategy not configured', async () => {
|
|
291
|
+
const containerWithoutStrategy: HandlerContext = {
|
|
292
|
+
resolve: jest.fn(() => []) as HandlerContext['resolve'],
|
|
293
|
+
}
|
|
294
|
+
const job = createMockJob<FulltextIndexJobPayload>({
|
|
295
|
+
jobType: 'batch-index',
|
|
296
|
+
tenantId: 'tenant-123',
|
|
297
|
+
records: [{ entityId: 'test', recordId: '1' }],
|
|
298
|
+
})
|
|
299
|
+
const ctx = createMockJobContext()
|
|
300
|
+
|
|
301
|
+
await handleFulltextIndexJob(job, ctx, containerWithoutStrategy)
|
|
302
|
+
|
|
303
|
+
expect(mockFulltextStrategy.bulkIndex).not.toHaveBeenCalled()
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('should throw when fulltext search is not available', async () => {
|
|
307
|
+
mockFulltextStrategy.isAvailable.mockResolvedValueOnce(false)
|
|
308
|
+
const job = createMockJob<FulltextIndexJobPayload>({
|
|
309
|
+
jobType: 'batch-index',
|
|
310
|
+
tenantId: 'tenant-123',
|
|
311
|
+
records: [{ entityId: 'test', recordId: '1' }],
|
|
312
|
+
})
|
|
313
|
+
const ctx = createMockJobContext()
|
|
314
|
+
|
|
315
|
+
await expect(handleFulltextIndexJob(job, ctx, mockContainer)).rejects.toThrow(
|
|
316
|
+
'Fulltext search is not available'
|
|
317
|
+
)
|
|
318
|
+
})
|
|
319
|
+
})
|