@86d-app/search 0.0.23 → 0.0.24

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 (104) hide show
  1. package/dist/modules/search/src/__tests__/admin-settings.test.js +262 -0
  2. package/dist/modules/search/src/__tests__/controllers.test.js +853 -0
  3. package/dist/modules/search/src/__tests__/embedding-provider.test.js +150 -0
  4. package/dist/modules/search/src/__tests__/endpoint-security.test.js +250 -0
  5. package/dist/modules/search/src/__tests__/meilisearch-provider.test.js +318 -0
  6. package/dist/modules/search/src/__tests__/service-impl.test.js +703 -0
  7. package/dist/modules/search/src/__tests__/store-endpoints.test.js +295 -0
  8. package/dist/{admin/components/index.d.ts → modules/search/src/admin/components/index.jsx} +0 -1
  9. package/dist/modules/search/src/admin/components/search-analytics.jsx +230 -0
  10. package/dist/modules/search/src/admin/endpoints/analytics.js +9 -0
  11. package/dist/modules/search/src/admin/endpoints/bulk-index.js +26 -0
  12. package/dist/modules/search/src/admin/endpoints/click-analytics.js +9 -0
  13. package/dist/modules/search/src/admin/endpoints/get-settings.js +97 -0
  14. package/dist/modules/search/src/admin/endpoints/index-manage.js +32 -0
  15. package/dist/modules/search/src/admin/endpoints/index.js +21 -0
  16. package/dist/modules/search/src/admin/endpoints/popular.js +11 -0
  17. package/dist/modules/search/src/admin/endpoints/synonyms.js +30 -0
  18. package/dist/modules/search/src/admin/endpoints/zero-results.js +11 -0
  19. package/dist/modules/search/src/embedding-provider.js +77 -0
  20. package/dist/modules/search/src/index.js +75 -0
  21. package/dist/modules/search/src/meilisearch-provider.js +138 -0
  22. package/dist/modules/search/src/schema.js +61 -0
  23. package/dist/modules/search/src/service-impl.js +770 -0
  24. package/dist/modules/search/src/service.js +1 -0
  25. package/dist/modules/search/src/store/components/_hooks.js +10 -0
  26. package/dist/modules/search/src/store/components/index.jsx +9 -0
  27. package/dist/modules/search/src/store/components/search-bar.jsx +91 -0
  28. package/dist/modules/search/src/store/components/search-page.jsx +17 -0
  29. package/dist/modules/search/src/store/components/search-results.jsx +51 -0
  30. package/dist/modules/search/src/store/endpoints/click.js +15 -0
  31. package/dist/modules/search/src/store/endpoints/index.js +12 -0
  32. package/dist/modules/search/src/store/endpoints/recent.js +18 -0
  33. package/dist/modules/search/src/store/endpoints/search.js +57 -0
  34. package/dist/modules/search/src/store/endpoints/store-search.js +33 -0
  35. package/dist/modules/search/src/store/endpoints/suggest.js +12 -0
  36. package/package.json +1 -1
  37. package/src/__tests__/admin-settings.test.ts +367 -0
  38. package/src/__tests__/store-endpoints.test.ts +392 -0
  39. package/src/admin/endpoints/get-settings.ts +77 -0
  40. package/dist/__tests__/controllers.test.d.ts +0 -2
  41. package/dist/__tests__/controllers.test.d.ts.map +0 -1
  42. package/dist/__tests__/embedding-provider.test.d.ts +0 -2
  43. package/dist/__tests__/embedding-provider.test.d.ts.map +0 -1
  44. package/dist/__tests__/endpoint-security.test.d.ts +0 -2
  45. package/dist/__tests__/endpoint-security.test.d.ts.map +0 -1
  46. package/dist/__tests__/meilisearch-provider.test.d.ts +0 -2
  47. package/dist/__tests__/meilisearch-provider.test.d.ts.map +0 -1
  48. package/dist/__tests__/service-impl.test.d.ts +0 -2
  49. package/dist/__tests__/service-impl.test.d.ts.map +0 -1
  50. package/dist/admin/components/index.d.ts.map +0 -1
  51. package/dist/admin/components/search-analytics.d.ts +0 -2
  52. package/dist/admin/components/search-analytics.d.ts.map +0 -1
  53. package/dist/admin/endpoints/analytics.d.ts +0 -15
  54. package/dist/admin/endpoints/analytics.d.ts.map +0 -1
  55. package/dist/admin/endpoints/bulk-index.d.ts +0 -20
  56. package/dist/admin/endpoints/bulk-index.d.ts.map +0 -1
  57. package/dist/admin/endpoints/click-analytics.d.ts +0 -7
  58. package/dist/admin/endpoints/click-analytics.d.ts.map +0 -1
  59. package/dist/admin/endpoints/get-settings.d.ts +0 -17
  60. package/dist/admin/endpoints/get-settings.d.ts.map +0 -1
  61. package/dist/admin/endpoints/index-manage.d.ts +0 -26
  62. package/dist/admin/endpoints/index-manage.d.ts.map +0 -1
  63. package/dist/admin/endpoints/index.d.ts +0 -125
  64. package/dist/admin/endpoints/index.d.ts.map +0 -1
  65. package/dist/admin/endpoints/popular.d.ts +0 -10
  66. package/dist/admin/endpoints/popular.d.ts.map +0 -1
  67. package/dist/admin/endpoints/synonyms.d.ts +0 -30
  68. package/dist/admin/endpoints/synonyms.d.ts.map +0 -1
  69. package/dist/admin/endpoints/zero-results.d.ts +0 -10
  70. package/dist/admin/endpoints/zero-results.d.ts.map +0 -1
  71. package/dist/embedding-provider.d.ts +0 -28
  72. package/dist/embedding-provider.d.ts.map +0 -1
  73. package/dist/index.d.ts +0 -23
  74. package/dist/index.d.ts.map +0 -1
  75. package/dist/meilisearch-provider.d.ts +0 -104
  76. package/dist/meilisearch-provider.d.ts.map +0 -1
  77. package/dist/schema.d.ts +0 -133
  78. package/dist/schema.d.ts.map +0 -1
  79. package/dist/service-impl.d.ts +0 -6
  80. package/dist/service-impl.d.ts.map +0 -1
  81. package/dist/service.d.ts +0 -127
  82. package/dist/service.d.ts.map +0 -1
  83. package/dist/store/components/_hooks.d.ts +0 -6
  84. package/dist/store/components/_hooks.d.ts.map +0 -1
  85. package/dist/store/components/index.d.ts +0 -10
  86. package/dist/store/components/index.d.ts.map +0 -1
  87. package/dist/store/components/search-bar.d.ts +0 -7
  88. package/dist/store/components/search-bar.d.ts.map +0 -1
  89. package/dist/store/components/search-page.d.ts +0 -4
  90. package/dist/store/components/search-page.d.ts.map +0 -1
  91. package/dist/store/components/search-results.d.ts +0 -9
  92. package/dist/store/components/search-results.d.ts.map +0 -1
  93. package/dist/store/endpoints/click.d.ts +0 -14
  94. package/dist/store/endpoints/click.d.ts.map +0 -1
  95. package/dist/store/endpoints/index.d.ts +0 -85
  96. package/dist/store/endpoints/index.d.ts.map +0 -1
  97. package/dist/store/endpoints/recent.d.ts +0 -15
  98. package/dist/store/endpoints/recent.d.ts.map +0 -1
  99. package/dist/store/endpoints/search.d.ts +0 -36
  100. package/dist/store/endpoints/search.d.ts.map +0 -1
  101. package/dist/store/endpoints/store-search.d.ts +0 -16
  102. package/dist/store/endpoints/store-search.d.ts.map +0 -1
  103. package/dist/store/endpoints/suggest.d.ts +0 -11
  104. package/dist/store/endpoints/suggest.d.ts.map +0 -1
@@ -0,0 +1,262 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { getSettings } from "../admin/endpoints/get-settings";
3
+ // ── Helpers ──────────────────────────────────────────────────────────────────
4
+ function extractHandler(ep) {
5
+ const obj = ep;
6
+ const fn = typeof obj.handler === "function" ? obj.handler : ep;
7
+ return fn;
8
+ }
9
+ const handler = extractHandler(getSettings);
10
+ function createMockController(indexCount = 42) {
11
+ return { getIndexCount: vi.fn().mockResolvedValue(indexCount) };
12
+ }
13
+ function callGetSettings(opts, controllerOverrides) {
14
+ return handler({
15
+ context: {
16
+ options: opts,
17
+ controllers: {
18
+ search: createMockController(controllerOverrides?.indexCount ?? 42),
19
+ },
20
+ },
21
+ });
22
+ }
23
+ // ── Fetch mocks ──────────────────────────────────────────────────────────────
24
+ /** MeiliSearch health OK then stats OK */
25
+ function mockMeiliConnected(docCount = 156) {
26
+ const spy = vi.spyOn(globalThis, "fetch");
27
+ spy.mockResolvedValueOnce(new Response(JSON.stringify({ status: "available" }), { status: 200 }));
28
+ spy.mockResolvedValueOnce(new Response(JSON.stringify({
29
+ numberOfDocuments: docCount,
30
+ isIndexing: false,
31
+ fieldDistribution: { id: docCount, title: docCount },
32
+ }), { status: 200 }));
33
+ }
34
+ /** MeiliSearch health endpoint fails */
35
+ function mockMeiliUnreachable() {
36
+ vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("ECONNREFUSED"));
37
+ }
38
+ /** MeiliSearch health then stats then embedding models endpoint */
39
+ function mockMeiliConnectedAndEmbeddingOk(docCount = 156) {
40
+ const spy = vi.spyOn(globalThis, "fetch");
41
+ // MeiliSearch health
42
+ spy.mockResolvedValueOnce(new Response(JSON.stringify({ status: "available" }), { status: 200 }));
43
+ // MeiliSearch stats
44
+ spy.mockResolvedValueOnce(new Response(JSON.stringify({
45
+ numberOfDocuments: docCount,
46
+ isIndexing: false,
47
+ fieldDistribution: {},
48
+ }), { status: 200 }));
49
+ // Embedding /models
50
+ spy.mockResolvedValueOnce(new Response(JSON.stringify({ data: [{ id: "text-embedding-3-small" }] }), {
51
+ status: 200,
52
+ }));
53
+ }
54
+ function mockEmbeddingOk() {
55
+ vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(JSON.stringify({ data: [{ id: "text-embedding-3-small" }] }), {
56
+ status: 200,
57
+ }));
58
+ }
59
+ function mockEmbeddingError(status) {
60
+ vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(JSON.stringify({ error: { message: "Invalid API key" } }), {
61
+ status,
62
+ }));
63
+ }
64
+ // ── Setup ────────────────────────────────────────────────────────────────────
65
+ beforeEach(() => {
66
+ vi.restoreAllMocks();
67
+ });
68
+ afterEach(() => {
69
+ vi.restoreAllMocks();
70
+ });
71
+ // ── MeiliSearch connection status ────────────────────────────────────────────
72
+ describe("getSettings — MeiliSearch connection", () => {
73
+ it('returns "connected" with document count when MeiliSearch is healthy', async () => {
74
+ mockMeiliConnected(250);
75
+ const result = await callGetSettings({
76
+ meilisearchHost: "http://meili.test:7700",
77
+ meilisearchApiKey: "master-key-abc123",
78
+ });
79
+ const meili = result.meilisearch;
80
+ expect(meili.status).toBe("connected");
81
+ expect(meili.documentCount).toBe(250);
82
+ expect(meili.configured).toBe(true);
83
+ expect(meili.error).toBeUndefined();
84
+ });
85
+ it('returns "not_configured" when no MeiliSearch credentials', async () => {
86
+ const result = await callGetSettings({});
87
+ const meili = result.meilisearch;
88
+ expect(meili.status).toBe("not_configured");
89
+ expect(meili.configured).toBe(false);
90
+ expect(meili.documentCount).toBeUndefined();
91
+ });
92
+ it('returns "not_configured" when only host is set', async () => {
93
+ const result = await callGetSettings({
94
+ meilisearchHost: "http://meili.test:7700",
95
+ });
96
+ const meili = result.meilisearch;
97
+ expect(meili.status).toBe("not_configured");
98
+ expect(meili.configured).toBe(false);
99
+ });
100
+ it('returns "error" when MeiliSearch is unreachable', async () => {
101
+ mockMeiliUnreachable();
102
+ const result = await callGetSettings({
103
+ meilisearchHost: "http://meili.test:7700",
104
+ meilisearchApiKey: "master-key-abc123",
105
+ });
106
+ const meili = result.meilisearch;
107
+ expect(meili.status).toBe("error");
108
+ expect(meili.error).toBe("MeiliSearch instance is not reachable");
109
+ expect(meili.configured).toBe(true);
110
+ });
111
+ it("masks the API key in the response", async () => {
112
+ mockMeiliConnected();
113
+ const result = await callGetSettings({
114
+ meilisearchHost: "http://meili.test:7700",
115
+ meilisearchApiKey: "master-key-abc123",
116
+ });
117
+ const meili = result.meilisearch;
118
+ expect(typeof meili.apiKey).toBe("string");
119
+ expect(meili.apiKey).not.toContain("master-key-abc123");
120
+ expect(meili.apiKey.startsWith("mast")).toBe(true);
121
+ expect(meili.apiKey).toContain("•");
122
+ });
123
+ it("returns configured host and index UID", async () => {
124
+ mockMeiliConnected();
125
+ const result = await callGetSettings({
126
+ meilisearchHost: "http://meili.test:7700",
127
+ meilisearchApiKey: "master-key-abc123",
128
+ meilisearchIndexUid: "products",
129
+ });
130
+ const meili = result.meilisearch;
131
+ expect(meili.host).toBe("http://meili.test:7700");
132
+ expect(meili.indexUid).toBe("products");
133
+ });
134
+ it("defaults index UID to 'search'", async () => {
135
+ mockMeiliConnected();
136
+ const result = await callGetSettings({
137
+ meilisearchHost: "http://meili.test:7700",
138
+ meilisearchApiKey: "key123",
139
+ });
140
+ const meili = result.meilisearch;
141
+ expect(meili.indexUid).toBe("search");
142
+ });
143
+ });
144
+ // ── Embedding provider connection status ─────────────────────────────────────
145
+ describe("getSettings — embedding connection", () => {
146
+ it('returns "connected" when OpenAI API key is valid', async () => {
147
+ mockEmbeddingOk();
148
+ const result = await callGetSettings({
149
+ openaiApiKey: "sk-test-abc123",
150
+ });
151
+ const embeddings = result.embeddings;
152
+ expect(embeddings.status).toBe("connected");
153
+ expect(embeddings.provider).toBe("openai");
154
+ expect(embeddings.configured).toBe(true);
155
+ expect(embeddings.error).toBeUndefined();
156
+ });
157
+ it('returns "connected" when OpenRouter API key is valid', async () => {
158
+ mockEmbeddingOk();
159
+ const result = await callGetSettings({
160
+ openrouterApiKey: "sk-or-test-abc123",
161
+ });
162
+ const embeddings = result.embeddings;
163
+ expect(embeddings.status).toBe("connected");
164
+ expect(embeddings.provider).toBe("openrouter");
165
+ expect(embeddings.configured).toBe(true);
166
+ });
167
+ it('returns "not_configured" when no embedding keys', async () => {
168
+ const result = await callGetSettings({});
169
+ const embeddings = result.embeddings;
170
+ expect(embeddings.status).toBe("not_configured");
171
+ expect(embeddings.provider).toBeNull();
172
+ expect(embeddings.configured).toBe(false);
173
+ });
174
+ it('returns "error" when embedding API rejects the key', async () => {
175
+ mockEmbeddingError(401);
176
+ const result = await callGetSettings({
177
+ openaiApiKey: "sk-invalid-key",
178
+ });
179
+ const embeddings = result.embeddings;
180
+ expect(embeddings.status).toBe("error");
181
+ expect(embeddings.error).toContain("401");
182
+ });
183
+ it('returns "error" when embedding API is unreachable', async () => {
184
+ vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("Network failure"));
185
+ const result = await callGetSettings({
186
+ openaiApiKey: "sk-test-abc123",
187
+ });
188
+ const embeddings = result.embeddings;
189
+ expect(embeddings.status).toBe("error");
190
+ expect(embeddings.error).toContain("Network failure");
191
+ });
192
+ it("returns configured embedding model", async () => {
193
+ mockEmbeddingOk();
194
+ const result = await callGetSettings({
195
+ openaiApiKey: "sk-test-abc123",
196
+ embeddingModel: "text-embedding-ada-002",
197
+ });
198
+ const embeddings = result.embeddings;
199
+ expect(embeddings.model).toBe("text-embedding-ada-002");
200
+ });
201
+ it("defaults model to text-embedding-3-small", async () => {
202
+ const result = await callGetSettings({});
203
+ const embeddings = result.embeddings;
204
+ expect(embeddings.model).toBe("text-embedding-3-small");
205
+ });
206
+ it("prefers OpenAI when both keys are set", async () => {
207
+ mockEmbeddingOk();
208
+ const result = await callGetSettings({
209
+ openaiApiKey: "sk-openai",
210
+ openrouterApiKey: "sk-or-key",
211
+ });
212
+ const embeddings = result.embeddings;
213
+ expect(embeddings.provider).toBe("openai");
214
+ });
215
+ });
216
+ // ── Combined states ──────────────────────────────────────────────────────────
217
+ describe("getSettings — combined providers", () => {
218
+ it("returns both connected when both providers are healthy", async () => {
219
+ mockMeiliConnectedAndEmbeddingOk(100);
220
+ const result = await callGetSettings({
221
+ meilisearchHost: "http://meili.test:7700",
222
+ meilisearchApiKey: "master-key-abc123",
223
+ openaiApiKey: "sk-test-abc123",
224
+ });
225
+ const meili = result.meilisearch;
226
+ const embeddings = result.embeddings;
227
+ expect(meili.status).toBe("connected");
228
+ expect(embeddings.status).toBe("connected");
229
+ });
230
+ it("always returns indexCount from the controller", async () => {
231
+ const result = await callGetSettings({}, { indexCount: 99 });
232
+ expect(result.indexCount).toBe(99);
233
+ });
234
+ it("returns indexCount even when providers are not configured", async () => {
235
+ const result = await callGetSettings({}, { indexCount: 0 });
236
+ expect(result.indexCount).toBe(0);
237
+ });
238
+ });
239
+ // ── Embedding API URL routing ────────────────────────────────────────────────
240
+ describe("getSettings — embedding API URL routing", () => {
241
+ it("calls OpenAI models endpoint for openaiApiKey", async () => {
242
+ const spy = vi
243
+ .spyOn(globalThis, "fetch")
244
+ .mockResolvedValueOnce(new Response(JSON.stringify({ data: [] }), { status: 200 }));
245
+ await callGetSettings({ openaiApiKey: "sk-test" });
246
+ expect(spy).toHaveBeenCalledOnce();
247
+ const [url, opts] = spy.mock.calls[0];
248
+ expect(url).toBe("https://api.openai.com/v1/models");
249
+ expect(opts.headers).toEqual({
250
+ Authorization: "Bearer sk-test",
251
+ });
252
+ });
253
+ it("calls OpenRouter models endpoint for openrouterApiKey", async () => {
254
+ const spy = vi
255
+ .spyOn(globalThis, "fetch")
256
+ .mockResolvedValueOnce(new Response(JSON.stringify({ data: [] }), { status: 200 }));
257
+ await callGetSettings({ openrouterApiKey: "sk-or-test" });
258
+ expect(spy).toHaveBeenCalledOnce();
259
+ const [url] = spy.mock.calls[0];
260
+ expect(url).toBe("https://openrouter.ai/api/v1/models");
261
+ });
262
+ });