@open-mercato/search 0.6.6-develop.6296.1.5e8bdfee17 → 0.6.6-develop.6300.1.08ff40c897
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +16 -2
- package/dist/modules/search/__integration__/TC-SEARCH-010.spec.js +45 -0
- package/dist/modules/search/__integration__/TC-SEARCH-010.spec.js.map +7 -0
- package/dist/modules/search/__integration__/TC-SEARCH-011.spec.js +25 -0
- package/dist/modules/search/__integration__/TC-SEARCH-011.spec.js.map +7 -0
- package/dist/modules/search/api/embeddings/route.js +50 -6
- package/dist/modules/search/api/embeddings/route.js.map +2 -2
- package/dist/modules/search/api/search/global/route.js +1 -1
- package/dist/modules/search/api/search/global/route.js.map +2 -2
- package/dist/modules/search/api/settings/global-search/route.js +8 -5
- package/dist/modules/search/api/settings/global-search/route.js.map +2 -2
- package/dist/modules/search/di.js +3 -1
- package/dist/modules/search/di.js.map +2 -2
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +6 -3
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +2 -2
- package/dist/modules/search/i18n/de.json +3 -0
- package/dist/modules/search/i18n/en.json +3 -0
- package/dist/modules/search/i18n/es.json +3 -0
- package/dist/modules/search/i18n/pl.json +3 -0
- package/dist/modules/search/lib/auto-indexing.js +4 -1
- package/dist/modules/search/lib/auto-indexing.js.map +2 -2
- package/dist/modules/search/lib/embedding-config.js +40 -3
- package/dist/modules/search/lib/embedding-config.js.map +2 -2
- package/dist/modules/search/lib/global-search-config.js +25 -3
- package/dist/modules/search/lib/global-search-config.js.map +2 -2
- package/dist/modules/search/lib/provider-probe.js +108 -0
- package/dist/modules/search/lib/provider-probe.js.map +7 -0
- package/package.json +4 -4
- package/src/modules/search/__integration__/TC-SEARCH-010.spec.ts +74 -0
- package/src/modules/search/__integration__/TC-SEARCH-011.spec.ts +46 -0
- package/src/modules/search/api/embeddings/__tests__/route.ollama-base-url.test.ts +3 -0
- package/src/modules/search/api/embeddings/route.ts +62 -6
- package/src/modules/search/api/search/global/route.ts +2 -2
- package/src/modules/search/api/settings/global-search/route.ts +9 -4
- package/src/modules/search/di.ts +2 -0
- package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +21 -2
- package/src/modules/search/i18n/de.json +3 -0
- package/src/modules/search/i18n/en.json +3 -0
- package/src/modules/search/i18n/es.json +3 -0
- package/src/modules/search/i18n/pl.json +3 -0
- package/src/modules/search/lib/__tests__/provider-probe.test.ts +137 -0
- package/src/modules/search/lib/__tests__/search-settings-scope.test.ts +152 -0
- package/src/modules/search/lib/auto-indexing.ts +6 -3
- package/src/modules/search/lib/embedding-config.ts +58 -5
- package/src/modules/search/lib/global-search-config.ts +36 -4
- package/src/modules/search/lib/provider-probe.ts +143 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:search] found
|
|
1
|
+
[build:search] found 93 entry points
|
|
2
2
|
[build:search] built successfully
|
package/AGENTS.md
CHANGED
|
@@ -44,18 +44,32 @@ Strategies automatically become unavailable if their backend is not configured (
|
|
|
44
44
|
|
|
45
45
|
## Configure Global Search (Cmd+K)
|
|
46
46
|
|
|
47
|
+
Search settings (Cmd+K strategies, embedding provider/model, auto-index flag) are
|
|
48
|
+
**tenant-scoped**: each tenant reads/writes its own row and never overwrites another
|
|
49
|
+
tenant's settings. A tenant with no saved row inherits the instance default (legacy
|
|
50
|
+
global row) and finally the env-derived default; GET responses carry a `source` of
|
|
51
|
+
`tenant | instance | env`. Scope is always derived from the authenticated context,
|
|
52
|
+
never from the request body. The vector index itself (shared pgvector table) stays
|
|
53
|
+
instance-level; per-tenant scoping covers settings selection, not the stored vectors.
|
|
54
|
+
|
|
47
55
|
Set global search dialog strategies per-tenant via **Settings > Search** or the API:
|
|
48
56
|
|
|
49
57
|
```typescript
|
|
50
58
|
// Get current config
|
|
51
59
|
GET /api/search/settings/global-search
|
|
52
|
-
// Response: { "enabledStrategies": ["fulltext", "vector", "tokens"] }
|
|
60
|
+
// Response: { "enabledStrategies": ["fulltext", "vector", "tokens"], "source": "tenant" }
|
|
53
61
|
|
|
54
|
-
// Update config
|
|
62
|
+
// Update config (writes only this tenant's row)
|
|
55
63
|
POST /api/search/settings/global-search
|
|
56
64
|
// Body: { "enabledStrategies": ["fulltext", "tokens"] }
|
|
57
65
|
```
|
|
58
66
|
|
|
67
|
+
Provider availability is verified by an active, cached, fail-closed probe
|
|
68
|
+
(`embeddingProviderProbe`): Ollama is checked via `GET {OLLAMA_BASE_URL}/api/tags`
|
|
69
|
+
(no longer assumed reachable), key-based providers via env-key presence. The
|
|
70
|
+
embeddings GET returns per-provider `available`/`reason`; the embeddings POST
|
|
71
|
+
rejects selecting an unreachable provider with `409 { error, reason }`.
|
|
72
|
+
|
|
59
73
|
## Create a Search Configuration
|
|
60
74
|
|
|
61
75
|
### Place the file here
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
3
|
+
import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
4
|
+
const DEFAULT_STRATEGIES = ["fulltext", "vector", "tokens"];
|
|
5
|
+
test.describe("TC-SEARCH-010: tenant-scoped search settings expose a source", () => {
|
|
6
|
+
test("global-search GET exposes source and POST round-trips as a tenant override", async ({ request }) => {
|
|
7
|
+
let token = null;
|
|
8
|
+
let original = null;
|
|
9
|
+
try {
|
|
10
|
+
token = await getAuthToken(request, "admin");
|
|
11
|
+
const before = await apiRequest(request, "GET", "/api/search/settings/global-search", { token });
|
|
12
|
+
expect(before.ok(), "admin should read global-search settings").toBeTruthy();
|
|
13
|
+
const beforeBody = await readJsonSafe(before) ?? {};
|
|
14
|
+
expect(Array.isArray(beforeBody.enabledStrategies)).toBeTruthy();
|
|
15
|
+
original = beforeBody.enabledStrategies ?? DEFAULT_STRATEGIES;
|
|
16
|
+
expect(["tenant", "instance", "env"]).toContain(beforeBody.source);
|
|
17
|
+
const next = original.includes("vector") ? original.filter((strategy) => strategy !== "vector") : [...original, "vector"];
|
|
18
|
+
const safeNext = next.length > 0 ? next : ["tokens"];
|
|
19
|
+
const update = await apiRequest(request, "POST", "/api/search/settings/global-search", {
|
|
20
|
+
token,
|
|
21
|
+
data: { enabledStrategies: safeNext }
|
|
22
|
+
});
|
|
23
|
+
expect(update.ok(), "admin should update global-search settings").toBeTruthy();
|
|
24
|
+
const after = await apiRequest(request, "GET", "/api/search/settings/global-search", { token });
|
|
25
|
+
const afterBody = await readJsonSafe(after) ?? {};
|
|
26
|
+
expect(afterBody.enabledStrategies?.sort()).toEqual([...safeNext].sort());
|
|
27
|
+
expect(afterBody.source, "after a tenant save the source is the tenant row").toBe("tenant");
|
|
28
|
+
} finally {
|
|
29
|
+
if (token && original) {
|
|
30
|
+
await apiRequest(request, "POST", "/api/search/settings/global-search", {
|
|
31
|
+
token,
|
|
32
|
+
data: { enabledStrategies: original }
|
|
33
|
+
}).catch(() => void 0);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
test("embeddings GET exposes the embedding config source", async ({ request }) => {
|
|
38
|
+
const token = await getAuthToken(request, "admin");
|
|
39
|
+
const response = await apiRequest(request, "GET", "/api/search/embeddings", { token });
|
|
40
|
+
expect(response.ok(), "admin should read embedding settings").toBeTruthy();
|
|
41
|
+
const body = await readJsonSafe(response) ?? {};
|
|
42
|
+
expect(["tenant", "instance", "env"]).toContain(body.settings?.embeddingConfigSource);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=TC-SEARCH-010.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/search/__integration__/TC-SEARCH-010.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\n\ntype GlobalSearchSettings = {\n enabledStrategies?: string[]\n source?: 'tenant' | 'instance' | 'env'\n}\ntype EmbeddingsSettings = {\n settings?: {\n embeddingConfig?: unknown\n embeddingConfigSource?: 'tenant' | 'instance' | 'env'\n }\n}\n\nconst DEFAULT_STRATEGIES = ['fulltext', 'vector', 'tokens']\n\n/**\n * TC-SEARCH-010: search settings carry a tenant-scope source discriminator.\n * Source: .ai/specs/2026-06-15-tenant-scoped-search-settings.md (Phase 2).\n *\n * The settings APIs are tenant-scoped: GET reports a `source` of tenant | instance\n * | env, and a tenant admin's POST writes only its own scoped row (subsequent GET\n * reports `source: 'tenant'`). This spec is self-contained \u2014 it restores the\n * tenant's original global-search strategies in `finally`.\n */\ntest.describe('TC-SEARCH-010: tenant-scoped search settings expose a source', () => {\n test('global-search GET exposes source and POST round-trips as a tenant override', async ({ request }) => {\n let token: string | null = null\n let original: string[] | null = null\n\n try {\n token = await getAuthToken(request, 'admin')\n\n const before = await apiRequest(request, 'GET', '/api/search/settings/global-search', { token })\n expect(before.ok(), 'admin should read global-search settings').toBeTruthy()\n const beforeBody = (await readJsonSafe<GlobalSearchSettings>(before)) ?? {}\n expect(Array.isArray(beforeBody.enabledStrategies)).toBeTruthy()\n original = beforeBody.enabledStrategies ?? DEFAULT_STRATEGIES\n expect(['tenant', 'instance', 'env']).toContain(beforeBody.source)\n\n const next = original.includes('vector')\n ? original.filter((strategy) => strategy !== 'vector')\n : [...original, 'vector']\n const safeNext = next.length > 0 ? next : ['tokens']\n\n const update = await apiRequest(request, 'POST', '/api/search/settings/global-search', {\n token,\n data: { enabledStrategies: safeNext },\n })\n expect(update.ok(), 'admin should update global-search settings').toBeTruthy()\n\n const after = await apiRequest(request, 'GET', '/api/search/settings/global-search', { token })\n const afterBody = (await readJsonSafe<GlobalSearchSettings>(after)) ?? {}\n expect(afterBody.enabledStrategies?.sort()).toEqual([...safeNext].sort())\n expect(afterBody.source, 'after a tenant save the source is the tenant row').toBe('tenant')\n } finally {\n if (token && original) {\n await apiRequest(request, 'POST', '/api/search/settings/global-search', {\n token,\n data: { enabledStrategies: original },\n }).catch(() => undefined)\n }\n }\n })\n\n test('embeddings GET exposes the embedding config source', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n const response = await apiRequest(request, 'GET', '/api/search/embeddings', { token })\n expect(response.ok(), 'admin should read embedding settings').toBeTruthy()\n const body = (await readJsonSafe<EmbeddingsSettings>(response)) ?? {}\n expect(['tenant', 'instance', 'env']).toContain(body.settings?.embeddingConfigSource)\n })\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AACzC,SAAS,oBAAoB;AAa7B,MAAM,qBAAqB,CAAC,YAAY,UAAU,QAAQ;AAW1D,KAAK,SAAS,gEAAgE,MAAM;AAClF,OAAK,8EAA8E,OAAO,EAAE,QAAQ,MAAM;AACxG,QAAI,QAAuB;AAC3B,QAAI,WAA4B;AAEhC,QAAI;AACF,cAAQ,MAAM,aAAa,SAAS,OAAO;AAE3C,YAAM,SAAS,MAAM,WAAW,SAAS,OAAO,sCAAsC,EAAE,MAAM,CAAC;AAC/F,aAAO,OAAO,GAAG,GAAG,0CAA0C,EAAE,WAAW;AAC3E,YAAM,aAAc,MAAM,aAAmC,MAAM,KAAM,CAAC;AAC1E,aAAO,MAAM,QAAQ,WAAW,iBAAiB,CAAC,EAAE,WAAW;AAC/D,iBAAW,WAAW,qBAAqB;AAC3C,aAAO,CAAC,UAAU,YAAY,KAAK,CAAC,EAAE,UAAU,WAAW,MAAM;AAEjE,YAAM,OAAO,SAAS,SAAS,QAAQ,IACnC,SAAS,OAAO,CAAC,aAAa,aAAa,QAAQ,IACnD,CAAC,GAAG,UAAU,QAAQ;AAC1B,YAAM,WAAW,KAAK,SAAS,IAAI,OAAO,CAAC,QAAQ;AAEnD,YAAM,SAAS,MAAM,WAAW,SAAS,QAAQ,sCAAsC;AAAA,QACrF;AAAA,QACA,MAAM,EAAE,mBAAmB,SAAS;AAAA,MACtC,CAAC;AACD,aAAO,OAAO,GAAG,GAAG,4CAA4C,EAAE,WAAW;AAE7E,YAAM,QAAQ,MAAM,WAAW,SAAS,OAAO,sCAAsC,EAAE,MAAM,CAAC;AAC9F,YAAM,YAAa,MAAM,aAAmC,KAAK,KAAM,CAAC;AACxE,aAAO,UAAU,mBAAmB,KAAK,CAAC,EAAE,QAAQ,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC;AACxE,aAAO,UAAU,QAAQ,kDAAkD,EAAE,KAAK,QAAQ;AAAA,IAC5F,UAAE;AACA,UAAI,SAAS,UAAU;AACrB,cAAM,WAAW,SAAS,QAAQ,sCAAsC;AAAA,UACtE;AAAA,UACA,MAAM,EAAE,mBAAmB,SAAS;AAAA,QACtC,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,MAC1B;AAAA,IACF;AAAA,EACF,CAAC;AAED,OAAK,sDAAsD,OAAO,EAAE,QAAQ,MAAM;AAChF,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AACjD,UAAM,WAAW,MAAM,WAAW,SAAS,OAAO,0BAA0B,EAAE,MAAM,CAAC;AACrF,WAAO,SAAS,GAAG,GAAG,sCAAsC,EAAE,WAAW;AACzE,UAAM,OAAQ,MAAM,aAAiC,QAAQ,KAAM,CAAC;AACpE,WAAO,CAAC,UAAU,YAAY,KAAK,CAAC,EAAE,UAAU,KAAK,UAAU,qBAAqB;AAAA,EACtF,CAAC;AACH,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
3
|
+
import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
4
|
+
test.describe("TC-SEARCH-011: save guard rejects an unavailable provider", () => {
|
|
5
|
+
test("selecting Ollama while it is unreachable is rejected with 409 + reason", async ({ request }) => {
|
|
6
|
+
const token = await getAuthToken(request, "admin");
|
|
7
|
+
const settingsResp = await apiRequest(request, "GET", "/api/search/embeddings", { token });
|
|
8
|
+
expect(settingsResp.ok(), "admin should read embedding settings").toBeTruthy();
|
|
9
|
+
const settings = await readJsonSafe(settingsResp) ?? {};
|
|
10
|
+
const availability = settings.settings?.providerAvailability ?? [];
|
|
11
|
+
expect(Array.isArray(availability)).toBeTruthy();
|
|
12
|
+
const ollama = availability.find((entry) => entry.providerId === "ollama");
|
|
13
|
+
test.skip(!ollama, "provider availability not exposed by this build");
|
|
14
|
+
test.skip(ollama?.available === true, "Ollama is reachable in this environment; cannot exercise the rejection path");
|
|
15
|
+
const response = await apiRequest(request, "POST", "/api/search/embeddings", {
|
|
16
|
+
token,
|
|
17
|
+
data: { embeddingConfig: { providerId: "ollama", model: "nomic-embed-text", dimension: 768 } }
|
|
18
|
+
});
|
|
19
|
+
expect(response.status(), "an unavailable provider must be rejected").toBe(409);
|
|
20
|
+
const body = await readJsonSafe(response) ?? {};
|
|
21
|
+
expect(typeof body.error, "the rejection carries an error message").toBe("string");
|
|
22
|
+
expect(body, "the rejection exposes a structured reason").toHaveProperty("reason");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
//# sourceMappingURL=TC-SEARCH-011.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/search/__integration__/TC-SEARCH-011.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\n\ntype ProviderAvailabilityEntry = { providerId: string; available: boolean; reason?: string }\ntype EmbeddingsSettings = {\n settings?: {\n providerAvailability?: ProviderAvailabilityEntry[]\n }\n}\ntype SaveError = { error?: string; reason?: string }\n\n/**\n * TC-SEARCH-011: the embeddings save guard rejects an unreachable provider.\n * Source: .ai/specs/2026-06-15-tenant-scoped-search-settings.md (Phase 3/4).\n *\n * POST /api/search/embeddings must reject selecting a provider the availability\n * probe reports unreachable with a 409 + structured reason, so a dead provider\n * (e.g. Ollama with nothing listening) can never be persisted. This is read-only\n * on the failure path (no state changes), so it is self-contained.\n */\ntest.describe('TC-SEARCH-011: save guard rejects an unavailable provider', () => {\n test('selecting Ollama while it is unreachable is rejected with 409 + reason', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n\n const settingsResp = await apiRequest(request, 'GET', '/api/search/embeddings', { token })\n expect(settingsResp.ok(), 'admin should read embedding settings').toBeTruthy()\n const settings = (await readJsonSafe<EmbeddingsSettings>(settingsResp)) ?? {}\n const availability = settings.settings?.providerAvailability ?? []\n expect(Array.isArray(availability)).toBeTruthy()\n\n const ollama = availability.find((entry) => entry.providerId === 'ollama')\n test.skip(!ollama, 'provider availability not exposed by this build')\n test.skip(ollama?.available === true, 'Ollama is reachable in this environment; cannot exercise the rejection path')\n\n const response = await apiRequest(request, 'POST', '/api/search/embeddings', {\n token,\n data: { embeddingConfig: { providerId: 'ollama', model: 'nomic-embed-text', dimension: 768 } },\n })\n\n expect(response.status(), 'an unavailable provider must be rejected').toBe(409)\n const body = (await readJsonSafe<SaveError>(response)) ?? {}\n expect(typeof body.error, 'the rejection carries an error message').toBe('string')\n expect(body, 'the rejection exposes a structured reason').toHaveProperty('reason')\n })\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AACzC,SAAS,oBAAoB;AAmB7B,KAAK,SAAS,6DAA6D,MAAM;AAC/E,OAAK,0EAA0E,OAAO,EAAE,QAAQ,MAAM;AACpG,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AAEjD,UAAM,eAAe,MAAM,WAAW,SAAS,OAAO,0BAA0B,EAAE,MAAM,CAAC;AACzF,WAAO,aAAa,GAAG,GAAG,sCAAsC,EAAE,WAAW;AAC7E,UAAM,WAAY,MAAM,aAAiC,YAAY,KAAM,CAAC;AAC5E,UAAM,eAAe,SAAS,UAAU,wBAAwB,CAAC;AACjE,WAAO,MAAM,QAAQ,YAAY,CAAC,EAAE,WAAW;AAE/C,UAAM,SAAS,aAAa,KAAK,CAAC,UAAU,MAAM,eAAe,QAAQ;AACzE,SAAK,KAAK,CAAC,QAAQ,iDAAiD;AACpE,SAAK,KAAK,QAAQ,cAAc,MAAM,6EAA6E;AAEnH,UAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,0BAA0B;AAAA,MAC3E;AAAA,MACA,MAAM,EAAE,iBAAiB,EAAE,YAAY,UAAU,OAAO,oBAAoB,WAAW,IAAI,EAAE;AAAA,IAC/F,CAAC;AAED,WAAO,SAAS,OAAO,GAAG,0CAA0C,EAAE,KAAK,GAAG;AAC9E,UAAM,OAAQ,MAAM,aAAwB,QAAQ,KAAM,CAAC;AAC3D,WAAO,OAAO,KAAK,OAAO,wCAAwC,EAAE,KAAK,QAAQ;AACjF,WAAO,MAAM,2CAA2C,EAAE,eAAe,QAAQ;AAAA,EACnF,CAAC;AACH,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -5,11 +5,13 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
|
5
5
|
import { envDisablesAutoIndexing, resolveAutoIndexingEnabled, SEARCH_AUTO_INDEX_CONFIG_KEY } from "../../lib/auto-indexing.js";
|
|
6
6
|
import {
|
|
7
7
|
resolveEmbeddingConfig,
|
|
8
|
+
resolveEmbeddingConfigResult,
|
|
8
9
|
saveEmbeddingConfig,
|
|
9
10
|
getConfiguredProviders,
|
|
10
11
|
detectConfigChange,
|
|
11
12
|
getEffectiveDimension
|
|
12
13
|
} from "../../lib/embedding-config.js";
|
|
14
|
+
import { checkAllProviders } from "../../lib/provider-probe.js";
|
|
13
15
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
14
16
|
import { EMBEDDING_PROVIDERS, DEFAULT_EMBEDDING_CONFIG } from "../../../../vector/index.js";
|
|
15
17
|
import {
|
|
@@ -43,6 +45,22 @@ const configUnavailable = async () => {
|
|
|
43
45
|
const { t } = await resolveTranslations();
|
|
44
46
|
return NextResponse.json({ error: t("search.api.errors.configUnavailable", "Configuration service unavailable") }, { status: 503 });
|
|
45
47
|
};
|
|
48
|
+
function resolveProbe(container) {
|
|
49
|
+
try {
|
|
50
|
+
return container.resolve("embeddingProviderProbe");
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function resolveProviderAvailability(container) {
|
|
56
|
+
const probe = resolveProbe(container);
|
|
57
|
+
if (!probe) return [];
|
|
58
|
+
try {
|
|
59
|
+
return await checkAllProviders(probe);
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
46
64
|
async function getIndexedDimension(container) {
|
|
47
65
|
try {
|
|
48
66
|
const drivers = container.resolve("vectorDrivers");
|
|
@@ -76,13 +94,16 @@ async function GET(req) {
|
|
|
76
94
|
let autoIndexingEnabled = !lockedByEnv;
|
|
77
95
|
if (!lockedByEnv) {
|
|
78
96
|
try {
|
|
79
|
-
autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true });
|
|
97
|
+
autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true, scope: { tenantId: auth.tenantId } });
|
|
80
98
|
} catch {
|
|
81
99
|
autoIndexingEnabled = true;
|
|
82
100
|
}
|
|
83
101
|
}
|
|
84
|
-
const embeddingConfig = await
|
|
102
|
+
const { config: embeddingConfig, source: embeddingConfigSource } = await resolveEmbeddingConfigResult(container, {
|
|
103
|
+
scope: { tenantId: auth.tenantId }
|
|
104
|
+
});
|
|
85
105
|
const configuredProviders = getConfiguredProviders();
|
|
106
|
+
const providerAvailability = await resolveProviderAvailability(container);
|
|
86
107
|
const indexedDimension = await getIndexedDimension(container);
|
|
87
108
|
const effectiveDimension = embeddingConfig ? getEffectiveDimension(embeddingConfig) : DEFAULT_EMBEDDING_CONFIG.dimension;
|
|
88
109
|
const reindexRequired = Boolean(
|
|
@@ -96,7 +117,9 @@ async function GET(req) {
|
|
|
96
117
|
autoIndexingLocked: lockedByEnv,
|
|
97
118
|
lockReason: lockedByEnv ? "env" : null,
|
|
98
119
|
embeddingConfig,
|
|
120
|
+
embeddingConfigSource,
|
|
99
121
|
configuredProviders,
|
|
122
|
+
providerAvailability,
|
|
100
123
|
indexedDimension,
|
|
101
124
|
reindexRequired,
|
|
102
125
|
documentCount
|
|
@@ -143,9 +166,9 @@ async function POST(req) {
|
|
|
143
166
|
{ status: 409 }
|
|
144
167
|
);
|
|
145
168
|
}
|
|
146
|
-
await service.setValue("vector", SEARCH_AUTO_INDEX_CONFIG_KEY, parsed.data.autoIndexingEnabled);
|
|
169
|
+
await service.setValue("vector", SEARCH_AUTO_INDEX_CONFIG_KEY, parsed.data.autoIndexingEnabled, { tenantId: auth.tenantId });
|
|
147
170
|
}
|
|
148
|
-
let embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null });
|
|
171
|
+
let embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null, scope: { tenantId: auth.tenantId } });
|
|
149
172
|
let reindexRequired = false;
|
|
150
173
|
let indexedDimension = await getIndexedDimension(container);
|
|
151
174
|
if (parsed.data.embeddingConfig) {
|
|
@@ -183,6 +206,22 @@ async function POST(req) {
|
|
|
183
206
|
throw err;
|
|
184
207
|
}
|
|
185
208
|
}
|
|
209
|
+
const probe = resolveProbe(container);
|
|
210
|
+
if (probe) {
|
|
211
|
+
const availability = await probe.checkAvailability(newConfig.providerId);
|
|
212
|
+
if (!availability.available) {
|
|
213
|
+
return NextResponse.json(
|
|
214
|
+
{
|
|
215
|
+
error: t(
|
|
216
|
+
"search.api.errors.providerUnavailable",
|
|
217
|
+
`Provider ${providerInfo.name} is not available: ${availability.reason ?? "unreachable"}`
|
|
218
|
+
),
|
|
219
|
+
reason: availability.reason ?? null
|
|
220
|
+
},
|
|
221
|
+
{ status: 409 }
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
186
225
|
const change = detectConfigChange(
|
|
187
226
|
embeddingConfig,
|
|
188
227
|
{
|
|
@@ -223,7 +262,7 @@ async function POST(req) {
|
|
|
223
262
|
);
|
|
224
263
|
}
|
|
225
264
|
}
|
|
226
|
-
await saveEmbeddingConfig(container, change.newConfig);
|
|
265
|
+
await saveEmbeddingConfig(container, change.newConfig, { scope: { tenantId: auth.tenantId } });
|
|
227
266
|
embeddingConfig = change.newConfig;
|
|
228
267
|
try {
|
|
229
268
|
const embeddingService = container.resolve("vectorEmbeddingService");
|
|
@@ -236,12 +275,15 @@ async function POST(req) {
|
|
|
236
275
|
let autoIndexingEnabled = !lockedByEnv;
|
|
237
276
|
if (!lockedByEnv) {
|
|
238
277
|
try {
|
|
239
|
-
autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true });
|
|
278
|
+
autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true, scope: { tenantId: auth.tenantId } });
|
|
240
279
|
} catch {
|
|
241
280
|
autoIndexingEnabled = true;
|
|
242
281
|
}
|
|
243
282
|
}
|
|
244
283
|
const updatedDocumentCount = auth.tenantId ? await getVectorDocumentCount(container, auth.tenantId, auth.orgId) : null;
|
|
284
|
+
const { source: embeddingConfigSource } = await resolveEmbeddingConfigResult(container, {
|
|
285
|
+
scope: { tenantId: auth.tenantId }
|
|
286
|
+
});
|
|
245
287
|
return toJson({
|
|
246
288
|
settings: {
|
|
247
289
|
openaiConfigured: openAiConfigured(),
|
|
@@ -249,7 +291,9 @@ async function POST(req) {
|
|
|
249
291
|
autoIndexingLocked: lockedByEnv,
|
|
250
292
|
lockReason: lockedByEnv ? "env" : null,
|
|
251
293
|
embeddingConfig,
|
|
294
|
+
embeddingConfigSource,
|
|
252
295
|
configuredProviders: getConfiguredProviders(),
|
|
296
|
+
providerAvailability: await resolveProviderAvailability(container),
|
|
253
297
|
indexedDimension,
|
|
254
298
|
reindexRequired,
|
|
255
299
|
documentCount: updatedDocumentCount
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/search/api/embeddings/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { envDisablesAutoIndexing, resolveAutoIndexingEnabled, SEARCH_AUTO_INDEX_CONFIG_KEY } from '../../lib/auto-indexing'\nimport {\n resolveEmbeddingConfig,\n saveEmbeddingConfig,\n getConfiguredProviders,\n detectConfigChange,\n getEffectiveDimension,\n} from '../../lib/embedding-config'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { EmbeddingProviderConfig, EmbeddingProviderId, VectorDriver } from '../../../../vector'\nimport { EMBEDDING_PROVIDERS, DEFAULT_EMBEDDING_CONFIG, EmbeddingService } from '../../../../vector'\nimport {\n assertSafeOllamaBaseUrl,\n UnsafeOllamaBaseUrlError,\n} from '../../../../vector/lib/ollama-url-safety'\nimport { searchDebug, searchDebugWarn, searchError } from '../../../../lib/debug'\nimport { embeddingsOpenApi } from '../openapi'\n\nconst embeddingConfigSchema = z.object({\n providerId: z.enum(['openai', 'google', 'mistral', 'cohere', 'bedrock', 'ollama']),\n model: z.string(),\n dimension: z.number(),\n outputDimensionality: z.number().optional(),\n baseUrl: z.string().optional(),\n})\n\nconst updateSchema = z.object({\n autoIndexingEnabled: z.boolean().optional(),\n embeddingConfig: embeddingConfigSchema.optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.embeddings.view'] },\n POST: { requireAuth: true, requireFeatures: ['search.embeddings.manage'] },\n}\n\ntype SettingsResponse = {\n settings: {\n openaiConfigured: boolean\n autoIndexingEnabled: boolean\n autoIndexingLocked: boolean\n lockReason: string | null\n embeddingConfig: EmbeddingProviderConfig | null\n configuredProviders: EmbeddingProviderId[]\n indexedDimension: number | null\n reindexRequired: boolean\n documentCount: number | null\n }\n}\n\nconst openAiConfigured = () => Boolean(process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY.trim().length > 0)\n\nconst toJson = (payload: SettingsResponse, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nconst configUnavailable = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('search.api.errors.configUnavailable', 'Configuration service unavailable') }, { status: 503 })\n}\n\nasync function getIndexedDimension(container: { resolve: <T = unknown>(name: string) => T }): Promise<number | null> {\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.getTableDimension) {\n return await pgvectorDriver.getTableDimension()\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getVectorDocumentCount(\n container: { resolve: <T = unknown>(name: string) => T },\n tenantId: string,\n organizationId?: string | null,\n): Promise<number | null> {\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.count) {\n return await pgvectorDriver.count({ tenantId, organizationId: organizationId ?? undefined })\n }\n return null\n } catch {\n return null\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n const container = await createRequestContainer()\n try {\n const lockedByEnv = envDisablesAutoIndexing()\n let autoIndexingEnabled = !lockedByEnv\n if (!lockedByEnv) {\n try {\n autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true })\n } catch {\n autoIndexingEnabled = true\n }\n }\n\n const embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null })\n const configuredProviders = getConfiguredProviders()\n const indexedDimension = await getIndexedDimension(container)\n\n const effectiveDimension = embeddingConfig\n ? getEffectiveDimension(embeddingConfig)\n : DEFAULT_EMBEDDING_CONFIG.dimension\n\n const reindexRequired = Boolean(\n indexedDimension &&\n embeddingConfig &&\n indexedDimension !== effectiveDimension\n )\n\n // Get document count for vector index\n const documentCount = auth.tenantId\n ? await getVectorDocumentCount(container, auth.tenantId, auth.orgId)\n : null\n\n return toJson({\n settings: {\n openaiConfigured: openAiConfigured(),\n autoIndexingEnabled: lockedByEnv ? false : autoIndexingEnabled,\n autoIndexingLocked: lockedByEnv,\n lockReason: lockedByEnv ? 'env' : null,\n embeddingConfig,\n configuredProviders,\n indexedDimension,\n reindexRequired,\n documentCount,\n },\n })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: t('api.errors.invalidJson', 'Invalid JSON payload.') }, { status: 400 })\n }\n const parsed = updateSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: t('api.errors.invalidPayload', 'Invalid payload.') }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n try {\n let service: ModuleConfigService\n try {\n service = (container.resolve('moduleConfigService') as ModuleConfigService)\n } catch {\n return await configUnavailable()\n }\n\n if (parsed.data.autoIndexingEnabled !== undefined) {\n if (envDisablesAutoIndexing()) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.autoIndexingDisabled',\n 'Auto-indexing is disabled via OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING (legacy alias: DISABLE_VECTOR_SEARCH_AUTOINDEXING).',\n ),\n },\n { status: 409 },\n )\n }\n await service.setValue('vector', SEARCH_AUTO_INDEX_CONFIG_KEY, parsed.data.autoIndexingEnabled)\n }\n\n let embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null })\n let reindexRequired = false\n let indexedDimension = await getIndexedDimension(container)\n\n if (parsed.data.embeddingConfig) {\n const newConfig = parsed.data.embeddingConfig\n const providerInfo = EMBEDDING_PROVIDERS[newConfig.providerId]\n\n if (!providerInfo) {\n return NextResponse.json(\n { error: t('search.api.errors.invalidProvider', 'Invalid embedding provider.') },\n { status: 400 },\n )\n }\n\n const configuredProviders = getConfiguredProviders()\n if (!configuredProviders.includes(newConfig.providerId)) {\n return NextResponse.json(\n { error: t('search.api.errors.providerNotConfigured', `Provider ${providerInfo.name} is not configured. Set ${providerInfo.envKeyRequired} environment variable.`) },\n { status: 400 },\n )\n }\n\n if (newConfig.providerId === 'ollama' && newConfig.baseUrl != null) {\n try {\n assertSafeOllamaBaseUrl(newConfig.baseUrl)\n } catch (err) {\n if (err instanceof UnsafeOllamaBaseUrlError) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.invalidOllamaBaseUrl',\n 'Ollama base URL is not allowed. Set OLLAMA_BASE_URL in the environment, or add the host to OM_SEARCH_OLLAMA_BASE_URL_ALLOWLIST.',\n ),\n reason: err.reason,\n },\n { status: 400 },\n )\n }\n throw err\n }\n }\n\n const change = detectConfigChange(\n embeddingConfig,\n {\n ...newConfig,\n updatedAt: new Date().toISOString(),\n },\n indexedDimension\n )\n\n if (change.requiresReindex) {\n const newDimension = getEffectiveDimension(change.newConfig)\n searchDebug('search.embeddings.update', 'config change detected, recreating table', {\n requiresReindex: change.requiresReindex,\n reason: change.reason,\n oldDimension: indexedDimension,\n newDimension,\n })\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.recreateWithDimension) {\n await pgvectorDriver.recreateWithDimension(newDimension)\n // Query the actual dimension from the database to confirm\n if (pgvectorDriver.getTableDimension) {\n indexedDimension = await pgvectorDriver.getTableDimension()\n } else {\n indexedDimension = newDimension\n }\n searchDebug('search.embeddings.update', 'table recreated successfully', { indexedDimension })\n } else {\n searchDebugWarn('search.embeddings.update', 'pgvector driver does not have recreateWithDimension method')\n }\n } catch (error) {\n searchError('search.embeddings.update', 'failed to recreate table', {\n error: error instanceof Error ? error.message : error,\n })\n return NextResponse.json(\n { error: t('search.api.errors.recreateFailed', 'Failed to recreate vector table with new dimension.') },\n { status: 500 },\n )\n }\n }\n\n await saveEmbeddingConfig(container, change.newConfig)\n embeddingConfig = change.newConfig\n\n try {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n embeddingService.updateConfig(embeddingConfig)\n } catch {\n // Embedding service may not be available in all contexts\n }\n\n reindexRequired = change.requiresReindex\n }\n\n const lockedByEnv = envDisablesAutoIndexing()\n let autoIndexingEnabled = !lockedByEnv\n if (!lockedByEnv) {\n try {\n autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true })\n } catch {\n autoIndexingEnabled = true\n }\n }\n\n // Get updated document count\n const updatedDocumentCount = auth.tenantId\n ? await getVectorDocumentCount(container, auth.tenantId, auth.orgId)\n : null\n\n return toJson({\n settings: {\n openaiConfigured: openAiConfigured(),\n autoIndexingEnabled: lockedByEnv ? false : autoIndexingEnabled,\n autoIndexingLocked: lockedByEnv,\n lockReason: lockedByEnv ? 'env' : null,\n embeddingConfig,\n configuredProviders: getConfiguredProviders(),\n indexedDimension,\n reindexRequired,\n documentCount: updatedDocumentCount,\n },\n })\n } catch (error) {\n searchError('search.embeddings.update', 'failed', {\n error: error instanceof Error ? error.message : error,\n })\n return NextResponse.json({ error: t('search.api.errors.updateFailed', 'Failed to update embedding settings.') }, { status: 500 })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = embeddingsOpenApi\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AAEnC,SAAS,yBAAyB,4BAA4B,oCAAoC;AAClG;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { envDisablesAutoIndexing, resolveAutoIndexingEnabled, SEARCH_AUTO_INDEX_CONFIG_KEY } from '../../lib/auto-indexing'\nimport {\n resolveEmbeddingConfig,\n resolveEmbeddingConfigResult,\n saveEmbeddingConfig,\n getConfiguredProviders,\n detectConfigChange,\n getEffectiveDimension,\n} from '../../lib/embedding-config'\nimport type { EmbeddingConfigSource } from '../../lib/embedding-config'\nimport { checkAllProviders } from '../../lib/provider-probe'\nimport type { EmbeddingProviderProbe, ProviderAvailabilityEntry } from '../../lib/provider-probe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { EmbeddingProviderConfig, EmbeddingProviderId, VectorDriver } from '../../../../vector'\nimport { EMBEDDING_PROVIDERS, DEFAULT_EMBEDDING_CONFIG, EmbeddingService } from '../../../../vector'\nimport {\n assertSafeOllamaBaseUrl,\n UnsafeOllamaBaseUrlError,\n} from '../../../../vector/lib/ollama-url-safety'\nimport { searchDebug, searchDebugWarn, searchError } from '../../../../lib/debug'\nimport { embeddingsOpenApi } from '../openapi'\n\nconst embeddingConfigSchema = z.object({\n providerId: z.enum(['openai', 'google', 'mistral', 'cohere', 'bedrock', 'ollama']),\n model: z.string(),\n dimension: z.number(),\n outputDimensionality: z.number().optional(),\n baseUrl: z.string().optional(),\n})\n\nconst updateSchema = z.object({\n autoIndexingEnabled: z.boolean().optional(),\n embeddingConfig: embeddingConfigSchema.optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.embeddings.view'] },\n POST: { requireAuth: true, requireFeatures: ['search.embeddings.manage'] },\n}\n\ntype SettingsResponse = {\n settings: {\n openaiConfigured: boolean\n autoIndexingEnabled: boolean\n autoIndexingLocked: boolean\n lockReason: string | null\n embeddingConfig: EmbeddingProviderConfig | null\n embeddingConfigSource: EmbeddingConfigSource\n configuredProviders: EmbeddingProviderId[]\n providerAvailability: ProviderAvailabilityEntry[]\n indexedDimension: number | null\n reindexRequired: boolean\n documentCount: number | null\n }\n}\n\nconst openAiConfigured = () => Boolean(process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY.trim().length > 0)\n\nconst toJson = (payload: SettingsResponse, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nconst configUnavailable = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('search.api.errors.configUnavailable', 'Configuration service unavailable') }, { status: 503 })\n}\n\nfunction resolveProbe(container: { resolve: <T = unknown>(name: string) => T }): EmbeddingProviderProbe | null {\n try {\n return container.resolve<EmbeddingProviderProbe>('embeddingProviderProbe')\n } catch {\n return null\n }\n}\n\nasync function resolveProviderAvailability(\n container: { resolve: <T = unknown>(name: string) => T },\n): Promise<ProviderAvailabilityEntry[]> {\n const probe = resolveProbe(container)\n if (!probe) return []\n try {\n return await checkAllProviders(probe)\n } catch {\n return []\n }\n}\n\nasync function getIndexedDimension(container: { resolve: <T = unknown>(name: string) => T }): Promise<number | null> {\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.getTableDimension) {\n return await pgvectorDriver.getTableDimension()\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getVectorDocumentCount(\n container: { resolve: <T = unknown>(name: string) => T },\n tenantId: string,\n organizationId?: string | null,\n): Promise<number | null> {\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.count) {\n return await pgvectorDriver.count({ tenantId, organizationId: organizationId ?? undefined })\n }\n return null\n } catch {\n return null\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n const container = await createRequestContainer()\n try {\n const lockedByEnv = envDisablesAutoIndexing()\n let autoIndexingEnabled = !lockedByEnv\n if (!lockedByEnv) {\n try {\n autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true, scope: { tenantId: auth.tenantId } })\n } catch {\n autoIndexingEnabled = true\n }\n }\n\n const { config: embeddingConfig, source: embeddingConfigSource } = await resolveEmbeddingConfigResult(container, {\n scope: { tenantId: auth.tenantId },\n })\n const configuredProviders = getConfiguredProviders()\n const providerAvailability = await resolveProviderAvailability(container)\n const indexedDimension = await getIndexedDimension(container)\n\n const effectiveDimension = embeddingConfig\n ? getEffectiveDimension(embeddingConfig)\n : DEFAULT_EMBEDDING_CONFIG.dimension\n\n const reindexRequired = Boolean(\n indexedDimension &&\n embeddingConfig &&\n indexedDimension !== effectiveDimension\n )\n\n // Get document count for vector index\n const documentCount = auth.tenantId\n ? await getVectorDocumentCount(container, auth.tenantId, auth.orgId)\n : null\n\n return toJson({\n settings: {\n openaiConfigured: openAiConfigured(),\n autoIndexingEnabled: lockedByEnv ? false : autoIndexingEnabled,\n autoIndexingLocked: lockedByEnv,\n lockReason: lockedByEnv ? 'env' : null,\n embeddingConfig,\n embeddingConfigSource,\n configuredProviders,\n providerAvailability,\n indexedDimension,\n reindexRequired,\n documentCount,\n },\n })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: t('api.errors.invalidJson', 'Invalid JSON payload.') }, { status: 400 })\n }\n const parsed = updateSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: t('api.errors.invalidPayload', 'Invalid payload.') }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n try {\n let service: ModuleConfigService\n try {\n service = (container.resolve('moduleConfigService') as ModuleConfigService)\n } catch {\n return await configUnavailable()\n }\n\n if (parsed.data.autoIndexingEnabled !== undefined) {\n if (envDisablesAutoIndexing()) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.autoIndexingDisabled',\n 'Auto-indexing is disabled via OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING (legacy alias: DISABLE_VECTOR_SEARCH_AUTOINDEXING).',\n ),\n },\n { status: 409 },\n )\n }\n await service.setValue('vector', SEARCH_AUTO_INDEX_CONFIG_KEY, parsed.data.autoIndexingEnabled, { tenantId: auth.tenantId })\n }\n\n let embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null, scope: { tenantId: auth.tenantId } })\n let reindexRequired = false\n let indexedDimension = await getIndexedDimension(container)\n\n if (parsed.data.embeddingConfig) {\n const newConfig = parsed.data.embeddingConfig\n const providerInfo = EMBEDDING_PROVIDERS[newConfig.providerId]\n\n if (!providerInfo) {\n return NextResponse.json(\n { error: t('search.api.errors.invalidProvider', 'Invalid embedding provider.') },\n { status: 400 },\n )\n }\n\n const configuredProviders = getConfiguredProviders()\n if (!configuredProviders.includes(newConfig.providerId)) {\n return NextResponse.json(\n { error: t('search.api.errors.providerNotConfigured', `Provider ${providerInfo.name} is not configured. Set ${providerInfo.envKeyRequired} environment variable.`) },\n { status: 400 },\n )\n }\n\n // Reject an unsafe user-supplied Ollama base URL before doing anything with it (SSRF guard).\n if (newConfig.providerId === 'ollama' && newConfig.baseUrl != null) {\n try {\n assertSafeOllamaBaseUrl(newConfig.baseUrl)\n } catch (err) {\n if (err instanceof UnsafeOllamaBaseUrlError) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.invalidOllamaBaseUrl',\n 'Ollama base URL is not allowed. Set OLLAMA_BASE_URL in the environment, or add the host to OM_SEARCH_OLLAMA_BASE_URL_ALLOWLIST.',\n ),\n reason: err.reason,\n },\n { status: 400 },\n )\n }\n throw err\n }\n }\n\n // Save-time availability guard: never persist a provider the probe reports unreachable.\n const probe = resolveProbe(container)\n if (probe) {\n const availability = await probe.checkAvailability(newConfig.providerId)\n if (!availability.available) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.providerUnavailable',\n `Provider ${providerInfo.name} is not available: ${availability.reason ?? 'unreachable'}`,\n ),\n reason: availability.reason ?? null,\n },\n { status: 409 },\n )\n }\n }\n\n const change = detectConfigChange(\n embeddingConfig,\n {\n ...newConfig,\n updatedAt: new Date().toISOString(),\n },\n indexedDimension\n )\n\n if (change.requiresReindex) {\n const newDimension = getEffectiveDimension(change.newConfig)\n searchDebug('search.embeddings.update', 'config change detected, recreating table', {\n requiresReindex: change.requiresReindex,\n reason: change.reason,\n oldDimension: indexedDimension,\n newDimension,\n })\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.recreateWithDimension) {\n await pgvectorDriver.recreateWithDimension(newDimension)\n // Query the actual dimension from the database to confirm\n if (pgvectorDriver.getTableDimension) {\n indexedDimension = await pgvectorDriver.getTableDimension()\n } else {\n indexedDimension = newDimension\n }\n searchDebug('search.embeddings.update', 'table recreated successfully', { indexedDimension })\n } else {\n searchDebugWarn('search.embeddings.update', 'pgvector driver does not have recreateWithDimension method')\n }\n } catch (error) {\n searchError('search.embeddings.update', 'failed to recreate table', {\n error: error instanceof Error ? error.message : error,\n })\n return NextResponse.json(\n { error: t('search.api.errors.recreateFailed', 'Failed to recreate vector table with new dimension.') },\n { status: 500 },\n )\n }\n }\n\n await saveEmbeddingConfig(container, change.newConfig, { scope: { tenantId: auth.tenantId } })\n embeddingConfig = change.newConfig\n\n try {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n embeddingService.updateConfig(embeddingConfig)\n } catch {\n // Embedding service may not be available in all contexts\n }\n\n reindexRequired = change.requiresReindex\n }\n\n const lockedByEnv = envDisablesAutoIndexing()\n let autoIndexingEnabled = !lockedByEnv\n if (!lockedByEnv) {\n try {\n autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true, scope: { tenantId: auth.tenantId } })\n } catch {\n autoIndexingEnabled = true\n }\n }\n\n // Get updated document count\n const updatedDocumentCount = auth.tenantId\n ? await getVectorDocumentCount(container, auth.tenantId, auth.orgId)\n : null\n\n const { source: embeddingConfigSource } = await resolveEmbeddingConfigResult(container, {\n scope: { tenantId: auth.tenantId },\n })\n\n return toJson({\n settings: {\n openaiConfigured: openAiConfigured(),\n autoIndexingEnabled: lockedByEnv ? false : autoIndexingEnabled,\n autoIndexingLocked: lockedByEnv,\n lockReason: lockedByEnv ? 'env' : null,\n embeddingConfig,\n embeddingConfigSource,\n configuredProviders: getConfiguredProviders(),\n providerAvailability: await resolveProviderAvailability(container),\n indexedDimension,\n reindexRequired,\n documentCount: updatedDocumentCount,\n },\n })\n } catch (error) {\n searchError('search.embeddings.update', 'failed', {\n error: error instanceof Error ? error.message : error,\n })\n return NextResponse.json({ error: t('search.api.errors.updateFailed', 'Failed to update embedding settings.') }, { status: 500 })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = embeddingsOpenApi\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -50,7 +50,7 @@ async function GET(req) {
|
|
|
50
50
|
{ status: 503 }
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
|
-
const strategies = await resolveGlobalSearchStrategies(container);
|
|
53
|
+
const strategies = await resolveGlobalSearchStrategies(container, { scope: { tenantId: auth.tenantId } });
|
|
54
54
|
if (strategies.includes("vector")) {
|
|
55
55
|
try {
|
|
56
56
|
const embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/search/api/search/global/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport type { SearchService } from '@open-mercato/search'\nimport type { EmbeddingService } from '../../../../../vector'\nimport { resolveEmbeddingConfig } from '../../../lib/embedding-config'\nimport { resolveGlobalSearchStrategies } from '../../../lib/global-search-config'\nimport { searchError } from '../../../../../lib/debug'\nimport { globalSearchOpenApi } from '../../openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.view'] },\n}\n\nfunction parseLimit(value: string | null): number {\n if (!value) return 50\n const parsed = Number.parseInt(value, 10)\n if (Number.isNaN(parsed) || parsed <= 0) return 50\n return Math.min(parsed, 100)\n}\n\nfunction parseEntityTypes(value: string | null): string[] | undefined {\n if (!value) return undefined\n const entityTypes = value.split(',').map((s) => s.trim()).filter(Boolean)\n return entityTypes.length > 0 ? entityTypes : undefined\n}\n\n/**\n * Global search endpoint for Cmd+K.\n * Always uses saved global search settings - does NOT accept strategies from URL.\n */\nexport async function GET(req: Request) {\n const { t } = await resolveTranslations()\n const url = new URL(req.url)\n const query = (url.searchParams.get('q') || '').trim()\n const limit = parseLimit(url.searchParams.get('limit'))\n const entityTypes = parseEntityTypes(url.searchParams.get('entityTypes'))\n\n if (!query) {\n return NextResponse.json(\n { error: t('search.api.errors.missingQuery', 'Missing query') },\n { status: 400 }\n )\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json(\n { error: t('api.errors.unauthorized', 'Unauthorized') },\n { status: 401 }\n )\n }\n\n const container = await createRequestContainer()\n try {\n const searchService = container.resolve('searchService') as SearchService | undefined\n if (!searchService) {\n return NextResponse.json(\n { error: t('search.api.errors.serviceUnavailable', 'Search service unavailable') },\n { status: 503 }\n )\n }\n\n // Fetch saved global search strategies\n const strategies = await resolveGlobalSearchStrategies(container)\n\n // Load embedding config for vector strategy (only if vector is enabled)\n if (strategies.includes('vector')) {\n try {\n const embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null })\n if (embeddingConfig) {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n embeddingService.updateConfig(embeddingConfig)\n }\n } catch {\n // Embedding config not available, vector strategy may not work\n }\n }\n\n const startTime = Date.now()\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n if (Array.isArray(scope.filterIds) && scope.filterIds.length === 0) {\n return NextResponse.json({\n results: [],\n strategiesUsed: [],\n strategiesEnabled: strategies,\n timing: 0,\n query,\n limit,\n })\n }\n\n const scopeFilter = resolveOrganizationScopeFilter(scope, auth)\n const organizationId =\n typeof scope.selectedId === 'string' && scope.selectedId.trim().length > 0 ? scope.selectedId.trim() : undefined\n const searchOptions = {\n tenantId: auth.tenantId,\n organizationId,\n organizationIds: scopeFilter.organizationIds,\n limit,\n strategies,\n entityTypes,\n }\n\n const results = await searchService.search(query, searchOptions)\n\n const timing = Date.now() - startTime\n\n // Collect unique strategies that returned results\n const strategiesUsed = [...new Set(results.map((r) => r.source))]\n\n return NextResponse.json({\n results,\n strategiesUsed,\n strategiesEnabled: strategies,\n timing,\n query,\n limit,\n })\n } catch (error: unknown) {\n searchError('search.api.global', 'failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n })\n return NextResponse.json(\n { error: t('search.api.errors.searchFailed', 'Search failed. Please try again.') },\n { status: 500 }\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = globalSearchOpenApi\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC,SAAS,0CAA0C;AACnD,SAAS,sCAAsC;AAG/C,SAAS,8BAA8B;AACvC,SAAS,qCAAqC;AAC9C,SAAS,mBAAmB;AAC5B,SAAS,2BAA2B;AAE7B,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,aAAa,EAAE;AAC7D;AAEA,SAAS,WAAW,OAA8B;AAChD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,OAAO,SAAS,OAAO,EAAE;AACxC,MAAI,OAAO,MAAM,MAAM,KAAK,UAAU,EAAG,QAAO;AAChD,SAAO,KAAK,IAAI,QAAQ,GAAG;AAC7B;AAEA,SAAS,iBAAiB,OAA4C;AACpE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,cAAc,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AACxE,SAAO,YAAY,SAAS,IAAI,cAAc;AAChD;AAMA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,SAAS,IAAI,aAAa,IAAI,GAAG,KAAK,IAAI,KAAK;AACrD,QAAM,QAAQ,WAAW,IAAI,aAAa,IAAI,OAAO,CAAC;AACtD,QAAM,cAAc,iBAAiB,IAAI,aAAa,IAAI,aAAa,CAAC;AAExE,MAAI,CAAC,OAAO;AACV,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,EAAE,kCAAkC,eAAe,EAAE;AAAA,MAC9D,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,UAAU;AACnB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,EAAE,2BAA2B,cAAc,EAAE;AAAA,MACtD,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,MAAI;AACF,UAAM,gBAAgB,UAAU,QAAQ,eAAe;AACvD,QAAI,CAAC,eAAe;AAClB,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,EAAE,wCAAwC,4BAA4B,EAAE;AAAA,QACjF,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAGA,UAAM,aAAa,MAAM,8BAA8B,SAAS;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport type { SearchService } from '@open-mercato/search'\nimport type { EmbeddingService } from '../../../../../vector'\nimport { resolveEmbeddingConfig } from '../../../lib/embedding-config'\nimport { resolveGlobalSearchStrategies } from '../../../lib/global-search-config'\nimport { searchError } from '../../../../../lib/debug'\nimport { globalSearchOpenApi } from '../../openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.view'] },\n}\n\nfunction parseLimit(value: string | null): number {\n if (!value) return 50\n const parsed = Number.parseInt(value, 10)\n if (Number.isNaN(parsed) || parsed <= 0) return 50\n return Math.min(parsed, 100)\n}\n\nfunction parseEntityTypes(value: string | null): string[] | undefined {\n if (!value) return undefined\n const entityTypes = value.split(',').map((s) => s.trim()).filter(Boolean)\n return entityTypes.length > 0 ? entityTypes : undefined\n}\n\n/**\n * Global search endpoint for Cmd+K.\n * Always uses saved global search settings - does NOT accept strategies from URL.\n */\nexport async function GET(req: Request) {\n const { t } = await resolveTranslations()\n const url = new URL(req.url)\n const query = (url.searchParams.get('q') || '').trim()\n const limit = parseLimit(url.searchParams.get('limit'))\n const entityTypes = parseEntityTypes(url.searchParams.get('entityTypes'))\n\n if (!query) {\n return NextResponse.json(\n { error: t('search.api.errors.missingQuery', 'Missing query') },\n { status: 400 }\n )\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json(\n { error: t('api.errors.unauthorized', 'Unauthorized') },\n { status: 401 }\n )\n }\n\n const container = await createRequestContainer()\n try {\n const searchService = container.resolve('searchService') as SearchService | undefined\n if (!searchService) {\n return NextResponse.json(\n { error: t('search.api.errors.serviceUnavailable', 'Search service unavailable') },\n { status: 503 }\n )\n }\n\n // Fetch saved global search strategies (per-tenant; falls back to the instance default)\n const strategies = await resolveGlobalSearchStrategies(container, { scope: { tenantId: auth.tenantId } })\n\n // Load embedding config for vector strategy (only if vector is enabled)\n if (strategies.includes('vector')) {\n try {\n const embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null })\n if (embeddingConfig) {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n embeddingService.updateConfig(embeddingConfig)\n }\n } catch {\n // Embedding config not available, vector strategy may not work\n }\n }\n\n const startTime = Date.now()\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n if (Array.isArray(scope.filterIds) && scope.filterIds.length === 0) {\n return NextResponse.json({\n results: [],\n strategiesUsed: [],\n strategiesEnabled: strategies,\n timing: 0,\n query,\n limit,\n })\n }\n\n const scopeFilter = resolveOrganizationScopeFilter(scope, auth)\n const organizationId =\n typeof scope.selectedId === 'string' && scope.selectedId.trim().length > 0 ? scope.selectedId.trim() : undefined\n const searchOptions = {\n tenantId: auth.tenantId,\n organizationId,\n organizationIds: scopeFilter.organizationIds,\n limit,\n strategies,\n entityTypes,\n }\n\n const results = await searchService.search(query, searchOptions)\n\n const timing = Date.now() - startTime\n\n // Collect unique strategies that returned results\n const strategiesUsed = [...new Set(results.map((r) => r.source))]\n\n return NextResponse.json({\n results,\n strategiesUsed,\n strategiesEnabled: strategies,\n timing,\n query,\n limit,\n })\n } catch (error: unknown) {\n searchError('search.api.global', 'failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n })\n return NextResponse.json(\n { error: t('search.api.errors.searchFailed', 'Search failed. Please try again.') },\n { status: 500 }\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = globalSearchOpenApi\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,7 +4,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
4
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
5
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
resolveGlobalSearchStrategiesResult,
|
|
8
8
|
saveGlobalSearchStrategies,
|
|
9
9
|
DEFAULT_GLOBAL_SEARCH_STRATEGIES
|
|
10
10
|
} from "../../../lib/global-search-config.js";
|
|
@@ -26,10 +26,11 @@ async function GET(req) {
|
|
|
26
26
|
if (!auth?.sub) return await unauthorized();
|
|
27
27
|
const container = await createRequestContainer();
|
|
28
28
|
try {
|
|
29
|
-
const enabledStrategies = await
|
|
30
|
-
defaultValue: DEFAULT_GLOBAL_SEARCH_STRATEGIES
|
|
29
|
+
const { strategies: enabledStrategies, source } = await resolveGlobalSearchStrategiesResult(container, {
|
|
30
|
+
defaultValue: DEFAULT_GLOBAL_SEARCH_STRATEGIES,
|
|
31
|
+
scope: { tenantId: auth.tenantId }
|
|
31
32
|
});
|
|
32
|
-
return toJson({ enabledStrategies });
|
|
33
|
+
return toJson({ enabledStrategies, source });
|
|
33
34
|
} finally {
|
|
34
35
|
const disposable = container;
|
|
35
36
|
if (typeof disposable.dispose === "function") {
|
|
@@ -59,7 +60,9 @@ async function POST(req) {
|
|
|
59
60
|
}
|
|
60
61
|
const container = await createRequestContainer();
|
|
61
62
|
try {
|
|
62
|
-
await saveGlobalSearchStrategies(container, parsed.data.enabledStrategies
|
|
63
|
+
await saveGlobalSearchStrategies(container, parsed.data.enabledStrategies, {
|
|
64
|
+
scope: { tenantId: auth.tenantId }
|
|
65
|
+
});
|
|
63
66
|
return NextResponse.json({ ok: true, enabledStrategies: parsed.data.enabledStrategies });
|
|
64
67
|
} catch (error) {
|
|
65
68
|
return NextResponse.json(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/search/api/settings/global-search/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n resolveGlobalSearchStrategiesResult,\n saveGlobalSearchStrategies,\n DEFAULT_GLOBAL_SEARCH_STRATEGIES,\n} from '../../../lib/global-search-config'\nimport type { GlobalSearchSource } from '../../../lib/global-search-config'\nimport type { SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport { globalSearchSettingsOpenApi } from '../../openapi'\n\nconst updateSchema = z.object({\n enabledStrategies: z.array(z.enum(['fulltext', 'vector', 'tokens'])).min(1),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.view'] },\n POST: { requireAuth: true, requireFeatures: ['search.manage'] },\n}\n\ntype SettingsResponse = {\n enabledStrategies: SearchStrategyId[]\n source?: GlobalSearchSource\n}\n\nconst toJson = (payload: SettingsResponse, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n const container = await createRequestContainer()\n try {\n const { strategies: enabledStrategies, source } = await resolveGlobalSearchStrategiesResult(container, {\n defaultValue: DEFAULT_GLOBAL_SEARCH_STRATEGIES,\n scope: { tenantId: auth.tenantId },\n })\n\n return toJson({ enabledStrategies, source })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json(\n { error: t('api.errors.invalidPayload', 'Invalid request body') },\n { status: 400 }\n )\n }\n\n const parsed = updateSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: t('search.api.errors.invalidStrategies', 'Invalid strategies configuration') },\n { status: 400 }\n )\n }\n\n const container = await createRequestContainer()\n try {\n await saveGlobalSearchStrategies(container, parsed.data.enabledStrategies, {\n scope: { tenantId: auth.tenantId },\n })\n\n return NextResponse.json({ ok: true, enabledStrategies: parsed.data.enabledStrategies })\n } catch (error) {\n return NextResponse.json(\n { error: error instanceof Error ? error.message : t('api.errors.internal', 'Internal error') },\n { status: 500 }\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = globalSearchSettingsOpenApi\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,mCAAmC;AAE5C,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,mBAAmB,EAAE,MAAM,EAAE,KAAK,CAAC,YAAY,UAAU,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC;AAC5E,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,aAAa,EAAE;AAAA,EAC3D,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,eAAe,EAAE;AAChE;AAOA,MAAM,SAAS,CAAC,SAA2B,SAAwB,aAAa,KAAK,SAAS,IAAI;AAElG,MAAM,eAAe,YAAY;AAC/B,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,SAAO,aAAa,KAAK,EAAE,OAAO,EAAE,2BAA2B,cAAc,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AACnG;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,IAAK,QAAO,MAAM,aAAa;AAE1C,QAAM,YAAY,MAAM,uBAAuB;AAC/C,MAAI;AACF,UAAM,EAAE,YAAY,mBAAmB,OAAO,IAAI,MAAM,oCAAoC,WAAW;AAAA,MACrG,cAAc;AAAA,MACd,OAAO,EAAE,UAAU,KAAK,SAAS;AAAA,IACnC,CAAC;AAED,WAAO,OAAO,EAAE,mBAAmB,OAAO,CAAC;AAAA,EAC7C,UAAE;AACA,UAAM,aAAa;AACnB,QAAI,OAAO,WAAW,YAAY,YAAY;AAC5C,YAAM,WAAW,QAAQ;AAAA,IAC3B;AAAA,EACF;AACF;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,IAAK,QAAO,MAAM,aAAa;AAE1C,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,EAAE,6BAA6B,sBAAsB,EAAE;AAAA,MAChE,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,SAAS,aAAa,UAAU,IAAI;AAC1C,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,EAAE,uCAAuC,kCAAkC,EAAE;AAAA,MACtF,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,MAAI;AACF,UAAM,2BAA2B,WAAW,OAAO,KAAK,mBAAmB;AAAA,MACzE,OAAO,EAAE,UAAU,KAAK,SAAS;AAAA,IACnC,CAAC;AAED,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,mBAAmB,OAAO,KAAK,kBAAkB,CAAC;AAAA,EACzF,SAAS,OAAO;AACd,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,EAAE,uBAAuB,gBAAgB,EAAE;AAAA,MAC7F,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF,UAAE;AACA,UAAM,aAAa;AACnB,QAAI,OAAO,WAAW,YAAY,YAAY;AAC5C,YAAM,WAAW,QAAQ;AAAA,IAC3B;AAAA,EACF;AACF;AAEO,MAAM,UAAU;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,7 @@ import { getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/connection";
|
|
|
3
3
|
import { EmbeddingService, createPgVectorDriver, createChromaDbDriver, createQdrantDriver } from "../../vector/index.js";
|
|
4
4
|
import { createVectorIndexingQueue } from "../../queue/vector-indexing.js";
|
|
5
5
|
import { createFulltextIndexingQueue } from "../../queue/fulltext-indexing.js";
|
|
6
|
+
import { createEmbeddingProviderProbe } from "./lib/provider-probe.js";
|
|
6
7
|
function register(container) {
|
|
7
8
|
const embeddingService = new EmbeddingService();
|
|
8
9
|
const drivers = [
|
|
@@ -24,7 +25,8 @@ function register(container) {
|
|
|
24
25
|
vectorEmbeddingService: asValue(embeddingService),
|
|
25
26
|
vectorDrivers: asValue(drivers),
|
|
26
27
|
vectorIndexQueue: asValue(vectorIndexQueue),
|
|
27
|
-
fulltextIndexQueue: asValue(fulltextIndexQueue)
|
|
28
|
+
fulltextIndexQueue: asValue(fulltextIndexQueue),
|
|
29
|
+
embeddingProviderProbe: asValue(createEmbeddingProviderProbe(container))
|
|
28
30
|
});
|
|
29
31
|
}
|
|
30
32
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/search/di.ts"],
|
|
4
|
-
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { EmbeddingService, createPgVectorDriver, createChromaDbDriver, createQdrantDriver } from '../../vector'\nimport { createVectorIndexingQueue, type VectorIndexJobPayload } from '../../queue/vector-indexing'\nimport { createFulltextIndexingQueue, type FulltextIndexJobPayload } from '../../queue/fulltext-indexing'\nimport type { Queue } from '@open-mercato/queue'\n\n/**\n * Register search module dependencies.\n *\n * This registers:\n * - vectorEmbeddingService: EmbeddingService for creating embeddings\n * - vectorDrivers: Array of vector database drivers (pgvector, chromadb, qdrant)\n * - vectorIndexQueue: Queue for vector indexing jobs\n * - fulltextIndexQueue: Queue for fulltext indexing jobs\n *\n * Note: VectorIndexService is no longer registered here. Use SearchIndexer instead,\n * which is registered in the main search module DI (packages/search/src/di.ts).\n */\nexport function register(container: AppContainer) {\n const embeddingService = new EmbeddingService()\n const drivers = [\n createPgVectorDriver(),\n createChromaDbDriver(),\n createQdrantDriver(),\n ]\n\n // Create queues based on environment strategy\n const queueStrategy = (process.env.QUEUE_STRATEGY || 'local') as 'local' | 'async'\n const queueConnection = queueStrategy === 'async'\n ? { connection: { url: getRedisUrlOrThrow('QUEUE') } }\n : undefined\n\n const vectorIndexQueue: Queue<VectorIndexJobPayload> = createVectorIndexingQueue(\n queueStrategy,\n queueConnection,\n )\n\n const fulltextIndexQueue: Queue<FulltextIndexJobPayload> = createFulltextIndexingQueue(\n queueStrategy,\n queueConnection,\n )\n\n container.register({\n vectorEmbeddingService: asValue(embeddingService),\n vectorDrivers: asValue(drivers),\n vectorIndexQueue: asValue(vectorIndexQueue),\n fulltextIndexQueue: asValue(fulltextIndexQueue),\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe;AAExB,SAAS,0BAA0B;AACnC,SAAS,kBAAkB,sBAAsB,sBAAsB,0BAA0B;AACjG,SAAS,iCAA6D;AACtE,SAAS,mCAAiE;
|
|
4
|
+
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { EmbeddingService, createPgVectorDriver, createChromaDbDriver, createQdrantDriver } from '../../vector'\nimport { createVectorIndexingQueue, type VectorIndexJobPayload } from '../../queue/vector-indexing'\nimport { createFulltextIndexingQueue, type FulltextIndexJobPayload } from '../../queue/fulltext-indexing'\nimport { createEmbeddingProviderProbe } from './lib/provider-probe'\nimport type { Queue } from '@open-mercato/queue'\n\n/**\n * Register search module dependencies.\n *\n * This registers:\n * - vectorEmbeddingService: EmbeddingService for creating embeddings\n * - vectorDrivers: Array of vector database drivers (pgvector, chromadb, qdrant)\n * - vectorIndexQueue: Queue for vector indexing jobs\n * - fulltextIndexQueue: Queue for fulltext indexing jobs\n *\n * Note: VectorIndexService is no longer registered here. Use SearchIndexer instead,\n * which is registered in the main search module DI (packages/search/src/di.ts).\n */\nexport function register(container: AppContainer) {\n const embeddingService = new EmbeddingService()\n const drivers = [\n createPgVectorDriver(),\n createChromaDbDriver(),\n createQdrantDriver(),\n ]\n\n // Create queues based on environment strategy\n const queueStrategy = (process.env.QUEUE_STRATEGY || 'local') as 'local' | 'async'\n const queueConnection = queueStrategy === 'async'\n ? { connection: { url: getRedisUrlOrThrow('QUEUE') } }\n : undefined\n\n const vectorIndexQueue: Queue<VectorIndexJobPayload> = createVectorIndexingQueue(\n queueStrategy,\n queueConnection,\n )\n\n const fulltextIndexQueue: Queue<FulltextIndexJobPayload> = createFulltextIndexingQueue(\n queueStrategy,\n queueConnection,\n )\n\n container.register({\n vectorEmbeddingService: asValue(embeddingService),\n vectorDrivers: asValue(drivers),\n vectorIndexQueue: asValue(vectorIndexQueue),\n fulltextIndexQueue: asValue(fulltextIndexQueue),\n embeddingProviderProbe: asValue(createEmbeddingProviderProbe(container)),\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe;AAExB,SAAS,0BAA0B;AACnC,SAAS,kBAAkB,sBAAsB,sBAAsB,0BAA0B;AACjG,SAAS,iCAA6D;AACtE,SAAS,mCAAiE;AAC1E,SAAS,oCAAoC;AAetC,SAAS,SAAS,WAAyB;AAChD,QAAM,mBAAmB,IAAI,iBAAiB;AAC9C,QAAM,UAAU;AAAA,IACd,qBAAqB;AAAA,IACrB,qBAAqB;AAAA,IACrB,mBAAmB;AAAA,EACrB;AAGA,QAAM,gBAAiB,QAAQ,IAAI,kBAAkB;AACrD,QAAM,kBAAkB,kBAAkB,UACtC,EAAE,YAAY,EAAE,KAAK,mBAAmB,OAAO,EAAE,EAAE,IACnD;AAEJ,QAAM,mBAAiD;AAAA,IACrD;AAAA,IACA;AAAA,EACF;AAEA,QAAM,qBAAqD;AAAA,IACzD;AAAA,IACA;AAAA,EACF;AAEA,YAAU,SAAS;AAAA,IACjB,wBAAwB,QAAQ,gBAAgB;AAAA,IAChD,eAAe,QAAQ,OAAO;AAAA,IAC9B,kBAAkB,QAAQ,gBAAgB;AAAA,IAC1C,oBAAoB,QAAQ,kBAAkB;AAAA,IAC9C,wBAAwB,QAAQ,6BAA6B,SAAS,CAAC;AAAA,EACzE,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -330,10 +330,13 @@ function VectorSearchSection({
|
|
|
330
330
|
] }),
|
|
331
331
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
332
332
|
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold mb-2", children: t("search.settings.vector.providers", "Embedding Provider") }),
|
|
333
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-
|
|
333
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-1", children: t("search.settings.vector.providersHint", "Select a provider to generate embeddings. Only reachable providers can be selected.") }),
|
|
334
|
+
embeddingSettings?.embeddingConfigSource && embeddingSettings.embeddingConfigSource !== "tenant" && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-3", children: t("search.settings.vector.inheritingDefault", "This tenant is inheriting the instance/environment default. Saving a provider creates an override for this tenant.") }),
|
|
335
|
+
embeddingSettings?.embeddingConfigSource === "tenant" && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-3", children: t("search.settings.vector.usingTenantConfig", "This tenant is using its own embedding settings.") }),
|
|
334
336
|
/* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3 items-start", children: providerOptions.map((providerId) => {
|
|
335
337
|
const info = EMBEDDING_PROVIDERS[providerId];
|
|
336
|
-
const
|
|
338
|
+
const availability = embeddingSettings?.providerAvailability?.find((entry) => entry.providerId === providerId);
|
|
339
|
+
const isConfigured = availability ? availability.available : embeddingSettings?.configuredProviders?.includes(providerId);
|
|
337
340
|
const isSelected = displayProvider === providerId;
|
|
338
341
|
const isCurrentlySaved = savedProvider === providerId;
|
|
339
342
|
return /* @__PURE__ */ jsxs(
|
|
@@ -354,7 +357,7 @@ function VectorSearchSection({
|
|
|
354
357
|
info.models.length,
|
|
355
358
|
" ",
|
|
356
359
|
t("search.settings.vector.modelsAvailable", "models available")
|
|
357
|
-
] }) : /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: [
|
|
360
|
+
] }) : availability?.reason ? /* @__PURE__ */ jsx("p", { className: "text-xs text-status-warning-text mt-1", children: availability.reason }) : /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: [
|
|
358
361
|
t("search.settings.vector.setEnvVar", "Set"),
|
|
359
362
|
" ",
|
|
360
363
|
/* @__PURE__ */ jsx("code", { className: "font-mono text-overline bg-muted px-1 rounded", children: info.envKeyRequired })
|