@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.
Files changed (237) hide show
  1. package/AGENTS.md +678 -0
  2. package/build.mjs +92 -0
  3. package/dist/di.js +157 -0
  4. package/dist/di.js.map +7 -0
  5. package/dist/fulltext/drivers/index.js +21 -0
  6. package/dist/fulltext/drivers/index.js.map +7 -0
  7. package/dist/fulltext/drivers/meilisearch/index.js +320 -0
  8. package/dist/fulltext/drivers/meilisearch/index.js.map +7 -0
  9. package/dist/fulltext/index.js +7 -0
  10. package/dist/fulltext/index.js.map +7 -0
  11. package/dist/fulltext/types.js +1 -0
  12. package/dist/fulltext/types.js.map +7 -0
  13. package/dist/index.js +12 -0
  14. package/dist/index.js.map +7 -0
  15. package/dist/indexer/index.js +8 -0
  16. package/dist/indexer/index.js.map +7 -0
  17. package/dist/indexer/search-indexer.js +848 -0
  18. package/dist/indexer/search-indexer.js.map +7 -0
  19. package/dist/indexer/subscribers/delete.js +41 -0
  20. package/dist/indexer/subscribers/delete.js.map +7 -0
  21. package/dist/lib/debug.js +34 -0
  22. package/dist/lib/debug.js.map +7 -0
  23. package/dist/lib/fallback-presenter.js +107 -0
  24. package/dist/lib/fallback-presenter.js.map +7 -0
  25. package/dist/lib/field-policy.js +75 -0
  26. package/dist/lib/field-policy.js.map +7 -0
  27. package/dist/lib/index.js +19 -0
  28. package/dist/lib/index.js.map +7 -0
  29. package/dist/lib/merger.js +93 -0
  30. package/dist/lib/merger.js.map +7 -0
  31. package/dist/lib/presenter-enricher.js +192 -0
  32. package/dist/lib/presenter-enricher.js.map +7 -0
  33. package/dist/modules/search/acl.js +14 -0
  34. package/dist/modules/search/acl.js.map +7 -0
  35. package/dist/modules/search/ai-tools.js +284 -0
  36. package/dist/modules/search/ai-tools.js.map +7 -0
  37. package/dist/modules/search/api/embeddings/reindex/cancel/route.js +65 -0
  38. package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +7 -0
  39. package/dist/modules/search/api/embeddings/reindex/route.js +165 -0
  40. package/dist/modules/search/api/embeddings/reindex/route.js.map +7 -0
  41. package/dist/modules/search/api/embeddings/route.js +246 -0
  42. package/dist/modules/search/api/embeddings/route.js.map +7 -0
  43. package/dist/modules/search/api/index/route.js +245 -0
  44. package/dist/modules/search/api/index/route.js.map +7 -0
  45. package/dist/modules/search/api/reindex/cancel/route.js +65 -0
  46. package/dist/modules/search/api/reindex/cancel/route.js.map +7 -0
  47. package/dist/modules/search/api/reindex/route.js +332 -0
  48. package/dist/modules/search/api/reindex/route.js.map +7 -0
  49. package/dist/modules/search/api/search/global/route.js +100 -0
  50. package/dist/modules/search/api/search/global/route.js.map +7 -0
  51. package/dist/modules/search/api/search/route.js +101 -0
  52. package/dist/modules/search/api/search/route.js.map +7 -0
  53. package/dist/modules/search/api/settings/fulltext/route.js +55 -0
  54. package/dist/modules/search/api/settings/fulltext/route.js.map +7 -0
  55. package/dist/modules/search/api/settings/global-search/route.js +80 -0
  56. package/dist/modules/search/api/settings/global-search/route.js.map +7 -0
  57. package/dist/modules/search/api/settings/route.js +118 -0
  58. package/dist/modules/search/api/settings/route.js.map +7 -0
  59. package/dist/modules/search/api/settings/vector-store/route.js +77 -0
  60. package/dist/modules/search/api/settings/vector-store/route.js.map +7 -0
  61. package/dist/modules/search/backend/config/search/page.js +10 -0
  62. package/dist/modules/search/backend/config/search/page.js.map +7 -0
  63. package/dist/modules/search/backend/config/search/page.meta.js +24 -0
  64. package/dist/modules/search/backend/config/search/page.meta.js.map +7 -0
  65. package/dist/modules/search/cli.js +698 -0
  66. package/dist/modules/search/cli.js.map +7 -0
  67. package/dist/modules/search/di.js +32 -0
  68. package/dist/modules/search/di.js.map +7 -0
  69. package/dist/modules/search/frontend/components/GlobalSearchDialog.js +357 -0
  70. package/dist/modules/search/frontend/components/GlobalSearchDialog.js.map +7 -0
  71. package/dist/modules/search/frontend/components/HybridSearchTable.js +343 -0
  72. package/dist/modules/search/frontend/components/HybridSearchTable.js.map +7 -0
  73. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +303 -0
  74. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +7 -0
  75. package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +360 -0
  76. package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +7 -0
  77. package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js +101 -0
  78. package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js.map +7 -0
  79. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +608 -0
  80. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +7 -0
  81. package/dist/modules/search/frontend/index.js +9 -0
  82. package/dist/modules/search/frontend/index.js.map +7 -0
  83. package/dist/modules/search/frontend/utils.js +41 -0
  84. package/dist/modules/search/frontend/utils.js.map +7 -0
  85. package/dist/modules/search/i18n/de.json +61 -0
  86. package/dist/modules/search/i18n/en.json +72 -0
  87. package/dist/modules/search/i18n/es.json +61 -0
  88. package/dist/modules/search/i18n/pl.json +61 -0
  89. package/dist/modules/search/index.js +11 -0
  90. package/dist/modules/search/index.js.map +7 -0
  91. package/dist/modules/search/lib/auto-indexing.js +29 -0
  92. package/dist/modules/search/lib/auto-indexing.js.map +7 -0
  93. package/dist/modules/search/lib/embedding-config.js +131 -0
  94. package/dist/modules/search/lib/embedding-config.js.map +7 -0
  95. package/dist/modules/search/lib/global-search-config.js +45 -0
  96. package/dist/modules/search/lib/global-search-config.js.map +7 -0
  97. package/dist/modules/search/lib/reindex-lock.js +99 -0
  98. package/dist/modules/search/lib/reindex-lock.js.map +7 -0
  99. package/dist/modules/search/subscribers/fulltext_upsert.js +64 -0
  100. package/dist/modules/search/subscribers/fulltext_upsert.js.map +7 -0
  101. package/dist/modules/search/subscribers/vector_delete.js +58 -0
  102. package/dist/modules/search/subscribers/vector_delete.js.map +7 -0
  103. package/dist/modules/search/subscribers/vector_purge.js +142 -0
  104. package/dist/modules/search/subscribers/vector_purge.js.map +7 -0
  105. package/dist/modules/search/subscribers/vector_upsert.js +58 -0
  106. package/dist/modules/search/subscribers/vector_upsert.js.map +7 -0
  107. package/dist/modules/search/workers/fulltext-index.worker.js +240 -0
  108. package/dist/modules/search/workers/fulltext-index.worker.js.map +7 -0
  109. package/dist/modules/search/workers/vector-index.worker.js +234 -0
  110. package/dist/modules/search/workers/vector-index.worker.js.map +7 -0
  111. package/dist/queue/fulltext-indexing.js +15 -0
  112. package/dist/queue/fulltext-indexing.js.map +7 -0
  113. package/dist/queue/index.js +3 -0
  114. package/dist/queue/index.js.map +7 -0
  115. package/dist/queue/vector-indexing.js +15 -0
  116. package/dist/queue/vector-indexing.js.map +7 -0
  117. package/dist/service.js +286 -0
  118. package/dist/service.js.map +7 -0
  119. package/dist/strategies/fulltext.strategy.js +116 -0
  120. package/dist/strategies/fulltext.strategy.js.map +7 -0
  121. package/dist/strategies/index.js +12 -0
  122. package/dist/strategies/index.js.map +7 -0
  123. package/dist/strategies/token.strategy.js +80 -0
  124. package/dist/strategies/token.strategy.js.map +7 -0
  125. package/dist/strategies/vector.strategy.js +137 -0
  126. package/dist/strategies/vector.strategy.js.map +7 -0
  127. package/dist/types.js +1 -0
  128. package/dist/types.js.map +7 -0
  129. package/dist/vector/drivers/chromadb/index.js +44 -0
  130. package/dist/vector/drivers/chromadb/index.js.map +7 -0
  131. package/dist/vector/drivers/index.js +9 -0
  132. package/dist/vector/drivers/index.js.map +7 -0
  133. package/dist/vector/drivers/pgvector/index.js +509 -0
  134. package/dist/vector/drivers/pgvector/index.js.map +7 -0
  135. package/dist/vector/drivers/qdrant/index.js +44 -0
  136. package/dist/vector/drivers/qdrant/index.js.map +7 -0
  137. package/dist/vector/index.js +4 -0
  138. package/dist/vector/index.js.map +7 -0
  139. package/dist/vector/lib/vector-logs.js +33 -0
  140. package/dist/vector/lib/vector-logs.js.map +7 -0
  141. package/dist/vector/services/checksum.js +20 -0
  142. package/dist/vector/services/checksum.js.map +7 -0
  143. package/dist/vector/services/embedding.js +222 -0
  144. package/dist/vector/services/embedding.js.map +7 -0
  145. package/dist/vector/services/index.js +4 -0
  146. package/dist/vector/services/index.js.map +7 -0
  147. package/dist/vector/services/vector-index.service.js +960 -0
  148. package/dist/vector/services/vector-index.service.js.map +7 -0
  149. package/dist/vector/types/pg.d.js +1 -0
  150. package/dist/vector/types/pg.d.js.map +7 -0
  151. package/dist/vector/types.js +75 -0
  152. package/dist/vector/types.js.map +7 -0
  153. package/jest.config.cjs +19 -0
  154. package/package.json +142 -0
  155. package/src/__tests__/queue.test.ts +148 -0
  156. package/src/__tests__/service.test.ts +345 -0
  157. package/src/__tests__/workers.test.ts +319 -0
  158. package/src/di.ts +291 -0
  159. package/src/fulltext/drivers/index.ts +41 -0
  160. package/src/fulltext/drivers/meilisearch/index.ts +410 -0
  161. package/src/fulltext/index.ts +13 -0
  162. package/src/fulltext/types.ts +115 -0
  163. package/src/index.ts +36 -0
  164. package/src/indexer/index.ts +13 -0
  165. package/src/indexer/search-indexer.ts +1141 -0
  166. package/src/indexer/subscribers/delete.ts +49 -0
  167. package/src/lib/debug.ts +46 -0
  168. package/src/lib/fallback-presenter.ts +106 -0
  169. package/src/lib/field-policy.ts +169 -0
  170. package/src/lib/index.ts +13 -0
  171. package/src/lib/merger.ts +159 -0
  172. package/src/lib/presenter-enricher.ts +323 -0
  173. package/src/modules/search/README.md +694 -0
  174. package/src/modules/search/acl.ts +10 -0
  175. package/src/modules/search/ai-tools.ts +467 -0
  176. package/src/modules/search/api/embeddings/reindex/cancel/route.ts +77 -0
  177. package/src/modules/search/api/embeddings/reindex/route.ts +197 -0
  178. package/src/modules/search/api/embeddings/route.ts +304 -0
  179. package/src/modules/search/api/index/route.ts +297 -0
  180. package/src/modules/search/api/reindex/cancel/route.ts +77 -0
  181. package/src/modules/search/api/reindex/route.ts +419 -0
  182. package/src/modules/search/api/search/global/route.ts +120 -0
  183. package/src/modules/search/api/search/route.ts +121 -0
  184. package/src/modules/search/api/settings/fulltext/route.ts +82 -0
  185. package/src/modules/search/api/settings/global-search/route.ts +91 -0
  186. package/src/modules/search/api/settings/route.ts +187 -0
  187. package/src/modules/search/api/settings/vector-store/route.ts +105 -0
  188. package/src/modules/search/backend/config/search/page.meta.ts +22 -0
  189. package/src/modules/search/backend/config/search/page.tsx +12 -0
  190. package/src/modules/search/cli.ts +818 -0
  191. package/src/modules/search/di.ts +50 -0
  192. package/src/modules/search/frontend/components/GlobalSearchDialog.tsx +436 -0
  193. package/src/modules/search/frontend/components/HybridSearchTable.tsx +418 -0
  194. package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +476 -0
  195. package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +624 -0
  196. package/src/modules/search/frontend/components/sections/GlobalSearchSection.tsx +124 -0
  197. package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +943 -0
  198. package/src/modules/search/frontend/index.ts +3 -0
  199. package/src/modules/search/frontend/utils.ts +82 -0
  200. package/src/modules/search/i18n/de.json +61 -0
  201. package/src/modules/search/i18n/en.json +72 -0
  202. package/src/modules/search/i18n/es.json +61 -0
  203. package/src/modules/search/i18n/pl.json +61 -0
  204. package/src/modules/search/index.ts +9 -0
  205. package/src/modules/search/lib/auto-indexing.ts +35 -0
  206. package/src/modules/search/lib/embedding-config.ts +161 -0
  207. package/src/modules/search/lib/global-search-config.ts +69 -0
  208. package/src/modules/search/lib/reindex-lock.ts +201 -0
  209. package/src/modules/search/subscribers/fulltext_upsert.ts +83 -0
  210. package/src/modules/search/subscribers/vector_delete.ts +75 -0
  211. package/src/modules/search/subscribers/vector_purge.ts +161 -0
  212. package/src/modules/search/subscribers/vector_upsert.ts +75 -0
  213. package/src/modules/search/workers/fulltext-index.worker.ts +318 -0
  214. package/src/modules/search/workers/vector-index.worker.ts +292 -0
  215. package/src/queue/fulltext-indexing.ts +87 -0
  216. package/src/queue/index.ts +2 -0
  217. package/src/queue/vector-indexing.ts +66 -0
  218. package/src/service.ts +397 -0
  219. package/src/strategies/fulltext.strategy.ts +155 -0
  220. package/src/strategies/index.ts +17 -0
  221. package/src/strategies/token.strategy.ts +153 -0
  222. package/src/strategies/vector.strategy.ts +234 -0
  223. package/src/types.ts +38 -0
  224. package/src/vector/drivers/chromadb/index.ts +49 -0
  225. package/src/vector/drivers/index.ts +4 -0
  226. package/src/vector/drivers/pgvector/index.ts +627 -0
  227. package/src/vector/drivers/qdrant/index.ts +49 -0
  228. package/src/vector/index.ts +3 -0
  229. package/src/vector/lib/vector-logs.ts +46 -0
  230. package/src/vector/services/checksum.ts +18 -0
  231. package/src/vector/services/embedding.ts +275 -0
  232. package/src/vector/services/index.ts +3 -0
  233. package/src/vector/services/vector-index.service.ts +1234 -0
  234. package/src/vector/types/pg.d.ts +1 -0
  235. package/src/vector/types.ts +220 -0
  236. package/tsconfig.json +9 -0
  237. package/watch.mjs +6 -0
@@ -0,0 +1,297 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import type { SearchIndexer } from '../../../../indexer/search-indexer'
5
+ import type { SearchService } from '../../../../service'
6
+ import { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'
7
+ import { writeCoverageCounts } from '@open-mercato/core/modules/query_index/lib/coverage'
8
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
+ import type { VectorSearchStrategy } from '../../../../strategies/vector.strategy'
10
+ import type { EntityId } from '@open-mercato/shared/modules/entities'
11
+ import { searchDebugWarn, searchError } from '../../../../lib/debug'
12
+
13
+ export const metadata = {
14
+ GET: { requireAuth: true, requireFeatures: ['search.view'] },
15
+ DELETE: { requireAuth: true, requireFeatures: ['search.embeddings.manage'] },
16
+ }
17
+
18
+ function parseLimit(value: string | null): number {
19
+ if (!value) return 50
20
+ const parsed = Number.parseInt(value, 10)
21
+ if (Number.isNaN(parsed) || parsed <= 0) return 50
22
+ return Math.min(parsed, 200)
23
+ }
24
+
25
+ function parseOffset(value: string | null): number {
26
+ if (!value) return 0
27
+ const parsed = Number.parseInt(value, 10)
28
+ if (Number.isNaN(parsed) || parsed < 0) return 0
29
+ return parsed
30
+ }
31
+
32
+ export async function GET(req: Request) {
33
+ const { t } = await resolveTranslations()
34
+ const auth = await getAuthFromRequest(req)
35
+ if (!auth?.tenantId) {
36
+ return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
37
+ }
38
+
39
+ const url = new URL(req.url)
40
+ const entityIdParam = url.searchParams.get('entityId')
41
+ const limit = parseLimit(url.searchParams.get('limit'))
42
+ const offset = parseOffset(url.searchParams.get('offset'))
43
+
44
+ const container = await createRequestContainer()
45
+ try {
46
+ // Get the vector strategy from search service
47
+ let searchService: SearchService
48
+ try {
49
+ searchService = container.resolve('searchService') as SearchService
50
+ } catch {
51
+ return NextResponse.json(
52
+ { error: t('search.api.errors.serviceUnavailable', 'Search service unavailable') },
53
+ { status: 503 }
54
+ )
55
+ }
56
+
57
+ // Access vector strategy for listing entries
58
+ const strategies = searchService.getStrategies()
59
+ const vectorStrategy = strategies.find((s) => s.id === 'vector') as VectorSearchStrategy | undefined
60
+
61
+ if (!vectorStrategy) {
62
+ return NextResponse.json(
63
+ { error: t('search.api.errors.vectorUnavailable', 'Vector strategy not configured') },
64
+ { status: 503 }
65
+ )
66
+ }
67
+
68
+ const isAvailable = await vectorStrategy.isAvailable()
69
+ if (!isAvailable) {
70
+ return NextResponse.json(
71
+ { error: t('search.api.errors.vectorUnavailable', 'Vector strategy not available') },
72
+ { status: 503 }
73
+ )
74
+ }
75
+
76
+ // List vector entries via the strategy
77
+ const entries = await vectorStrategy.listEntries({
78
+ tenantId: auth.tenantId,
79
+ organizationId: auth.orgId ?? null,
80
+ entityId: entityIdParam ?? undefined,
81
+ limit,
82
+ offset,
83
+ })
84
+
85
+ return NextResponse.json({ entries, limit, offset })
86
+ } catch (error: unknown) {
87
+ const err = error as { status?: number; statusCode?: number }
88
+ const status = typeof err?.status === 'number'
89
+ ? err.status
90
+ : (typeof err?.statusCode === 'number' ? err.statusCode : 500)
91
+ searchError('search.index.list', 'failed', {
92
+ error: error instanceof Error ? error.message : error,
93
+ stack: error instanceof Error ? error.stack : undefined,
94
+ })
95
+ return NextResponse.json(
96
+ { error: t('search.api.errors.indexFetchFailed', 'Failed to fetch vector index. Please try again.') },
97
+ { status: status >= 400 ? status : 500 }
98
+ )
99
+ } finally {
100
+ const disposable = container as unknown as { dispose?: () => Promise<void> }
101
+ if (typeof disposable.dispose === 'function') {
102
+ await disposable.dispose()
103
+ }
104
+ }
105
+ }
106
+
107
+ export async function DELETE(req: Request) {
108
+ const { t } = await resolveTranslations()
109
+ const auth = await getAuthFromRequest(req)
110
+ if (!auth?.tenantId) {
111
+ return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
112
+ }
113
+
114
+ const url = new URL(req.url)
115
+ const entityIdParam = url.searchParams.get('entityId')
116
+ const confirmAll = url.searchParams.get('confirmAll') === 'true'
117
+
118
+ // Require explicit confirmation when purging ALL entities (dangerous operation)
119
+ if (!entityIdParam && !confirmAll) {
120
+ return NextResponse.json(
121
+ { error: t('search.api.errors.confirmAllRequired', 'Purging all entities requires confirmAll=true parameter.') },
122
+ { status: 400 }
123
+ )
124
+ }
125
+
126
+ const container = await createRequestContainer()
127
+ try {
128
+ let searchIndexer: SearchIndexer
129
+ try {
130
+ searchIndexer = container.resolve('searchIndexer') as SearchIndexer
131
+ } catch {
132
+ return NextResponse.json(
133
+ { error: t('search.api.errors.indexUnavailable', 'Search indexer unavailable') },
134
+ { status: 503 }
135
+ )
136
+ }
137
+
138
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
139
+ let em: any = null
140
+ try {
141
+ em = container.resolve('em')
142
+ } catch {
143
+ // em not available
144
+ }
145
+
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
+ let eventBus: { emitEvent(event: string, payload: any, options?: any): Promise<void> } | null = null
148
+ try {
149
+ eventBus = container.resolve('eventBus')
150
+ } catch {
151
+ eventBus = null
152
+ }
153
+
154
+ const entityIds = entityIdParam
155
+ ? [entityIdParam]
156
+ : searchIndexer.listEnabledEntities()
157
+
158
+ const scopes = new Set<string>()
159
+ const registerScope = (org: string | null) => {
160
+ const key = org ?? '__null__'
161
+ if (!scopes.has(key)) scopes.add(key)
162
+ }
163
+ registerScope(null)
164
+ if (auth.orgId) registerScope(auth.orgId)
165
+
166
+ await recordIndexerLog(
167
+ { em: em ?? undefined },
168
+ {
169
+ source: 'vector',
170
+ handler: 'api:search.index.purge',
171
+ message: entityIdParam
172
+ ? `Vector purge requested for ${entityIdParam}`
173
+ : 'Vector purge requested for all entities',
174
+ entityType: entityIdParam ?? null,
175
+ tenantId: auth.tenantId ?? null,
176
+ organizationId: auth.orgId ?? null,
177
+ details: { entityIds },
178
+ },
179
+ ).catch(() => undefined)
180
+
181
+ // Purge each entity using SearchIndexer
182
+ for (const entityId of entityIds) {
183
+ await searchIndexer.purgeEntity({
184
+ entityId: entityId as EntityId,
185
+ tenantId: auth.tenantId,
186
+ })
187
+ }
188
+
189
+ // Update coverage counts
190
+ if (em) {
191
+ try {
192
+ for (const entityId of entityIds) {
193
+ for (const scope of scopes) {
194
+ const orgValue = scope === '__null__' ? null : scope
195
+ await writeCoverageCounts(
196
+ em,
197
+ {
198
+ entityType: entityId,
199
+ tenantId: auth.tenantId,
200
+ organizationId: orgValue,
201
+ withDeleted: false,
202
+ },
203
+ { vectorCount: 0 },
204
+ )
205
+ }
206
+ }
207
+ } catch (coverageError) {
208
+ searchDebugWarn('search.index.purge', 'Failed to reset coverage after purge', {
209
+ error: coverageError instanceof Error ? coverageError.message : coverageError,
210
+ })
211
+ }
212
+ }
213
+
214
+ // Emit coverage refresh events
215
+ if (eventBus) {
216
+ await Promise.all(
217
+ entityIds.flatMap((entityId) =>
218
+ Array.from(scopes).map((scope) => {
219
+ const orgValue = scope === '__null__' ? null : scope
220
+ return eventBus!
221
+ .emitEvent(
222
+ 'query_index.coverage.refresh',
223
+ {
224
+ entityType: entityId,
225
+ tenantId: auth.tenantId,
226
+ organizationId: orgValue,
227
+ delayMs: 0,
228
+ },
229
+ )
230
+ .catch(() => undefined)
231
+ }),
232
+ ),
233
+ )
234
+ }
235
+
236
+ await recordIndexerLog(
237
+ { em: em ?? undefined },
238
+ {
239
+ source: 'vector',
240
+ handler: 'api:search.index.purge',
241
+ message: entityIdParam
242
+ ? `Vector purge completed for ${entityIdParam}`
243
+ : 'Vector purge completed for all entities',
244
+ entityType: entityIdParam ?? null,
245
+ tenantId: auth.tenantId ?? null,
246
+ organizationId: auth.orgId ?? null,
247
+ details: { entityIds },
248
+ },
249
+ ).catch(() => undefined)
250
+
251
+ return NextResponse.json({ ok: true })
252
+ } catch (error: unknown) {
253
+ const err = error as { status?: number; statusCode?: number }
254
+ const status = typeof err?.status === 'number'
255
+ ? err.status
256
+ : (typeof err?.statusCode === 'number' ? err.statusCode : 500)
257
+ const errorMessage = error instanceof Error ? error.message : String(error)
258
+ searchError('search.index.purge', 'failed', {
259
+ error: errorMessage,
260
+ stack: error instanceof Error ? error.stack : undefined,
261
+ })
262
+
263
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
264
+ let em: any = null
265
+ try {
266
+ em = container.resolve('em')
267
+ } catch {
268
+ // em not available
269
+ }
270
+
271
+ await recordIndexerLog(
272
+ { em: em ?? undefined },
273
+ {
274
+ source: 'vector',
275
+ handler: 'api:search.index.purge',
276
+ level: 'warn',
277
+ message: entityIdParam
278
+ ? `Vector purge failed for ${entityIdParam}`
279
+ : 'Vector purge failed for all entities',
280
+ entityType: entityIdParam ?? null,
281
+ tenantId: auth.tenantId ?? null,
282
+ organizationId: auth.orgId ?? null,
283
+ details: { error: errorMessage },
284
+ },
285
+ ).catch(() => undefined)
286
+
287
+ return NextResponse.json(
288
+ { error: t('search.api.errors.purgeFailed', 'Vector index purge failed. Please try again.') },
289
+ { status: status >= 400 ? status : 500 }
290
+ )
291
+ } finally {
292
+ const disposable = container as unknown as { dispose?: () => Promise<void> }
293
+ if (typeof disposable.dispose === 'function') {
294
+ await disposable.dispose()
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,77 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import type { Queue } from '@open-mercato/queue'
5
+ import type { Knex } from 'knex'
6
+ import type { EntityManager } from '@mikro-orm/postgresql'
7
+ import { clearReindexLock } from '../../../lib/reindex-lock'
8
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
+ import { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'
10
+
11
+ export const metadata = {
12
+ POST: { requireAuth: true, requireFeatures: ['search.reindex'] },
13
+ }
14
+
15
+ export async function POST(req: Request) {
16
+ const { t } = await resolveTranslations()
17
+ const auth = await getAuthFromRequest(req)
18
+ if (!auth?.tenantId) {
19
+ return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
20
+ }
21
+
22
+ const container = await createRequestContainer()
23
+ const em = container.resolve('em') as EntityManager
24
+ const knex = (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()
25
+
26
+ let queue: Queue | undefined
27
+ try {
28
+ queue = container.resolve<Queue>('fulltextIndexQueue')
29
+ } catch {
30
+ // Queue not available - just clear the lock
31
+ }
32
+
33
+ let jobsRemoved = 0
34
+ if (queue) {
35
+ try {
36
+ const countsBefore = await queue.getJobCounts()
37
+ jobsRemoved = countsBefore.waiting + countsBefore.active
38
+ await queue.clear()
39
+ } catch {
40
+ // Queue clear failed - continue to clear lock
41
+ }
42
+ }
43
+
44
+ await clearReindexLock(knex, auth.tenantId, 'fulltext', auth.orgId ?? null)
45
+
46
+ // Log the cancellation
47
+ try {
48
+ const em = container.resolve('em')
49
+ await recordIndexerLog(
50
+ { em },
51
+ {
52
+ source: 'fulltext',
53
+ handler: 'api:search.reindex.cancel',
54
+ message: `Cancelled fulltext reindex operation (${jobsRemoved} jobs removed)`,
55
+ tenantId: auth.tenantId,
56
+ organizationId: auth.orgId ?? null,
57
+ details: { jobsRemoved },
58
+ },
59
+ )
60
+ } catch {
61
+ // Logging failure should not fail the cancel operation
62
+ }
63
+
64
+ try {
65
+ const disposable = container as unknown as { dispose?: () => Promise<void> }
66
+ if (typeof disposable.dispose === 'function') {
67
+ await disposable.dispose()
68
+ }
69
+ } catch {
70
+ // Ignore disposal errors
71
+ }
72
+
73
+ return NextResponse.json({
74
+ ok: true,
75
+ jobsRemoved,
76
+ })
77
+ }