@toolsdk.ai/registry 1.0.102 → 1.0.103
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/dist/api/index.js +31 -3
- package/dist/api/package-handler.d.ts +4 -4
- package/dist/api/package-so.js +3 -0
- package/dist/helper.d.ts +1 -0
- package/dist/helper.js +9 -1
- package/dist/search/search-route.d.ts +3 -0
- package/dist/search/search-route.js +305 -0
- package/dist/search/search-service.d.ts +120 -0
- package/dist/search/search-service.js +417 -0
- package/dist/search/search.test.d.ts +1 -0
- package/dist/search/search.test.js +100 -0
- package/indexes/packages-list.json +1 -1
- package/package.json +584 -578
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeiliSearch Service for Awesome MCP Registry
|
|
3
|
+
* Handles search indexing and querying for MCP packages
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { MeiliSearch } from "meilisearch";
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
class SearchService {
|
|
12
|
+
constructor(indexName = "mcp-packages") {
|
|
13
|
+
// MeiliSearch configuration
|
|
14
|
+
this.host = process.env.MEILI_HTTP_ADDR || "http://localhost:7700";
|
|
15
|
+
this.apiKey = process.env.MEILI_MASTER_KEY || null;
|
|
16
|
+
this.indexName = indexName;
|
|
17
|
+
// Initialize MeiliSearch client
|
|
18
|
+
this.client = new MeiliSearch({
|
|
19
|
+
host: this.host,
|
|
20
|
+
apiKey: this.apiKey || undefined,
|
|
21
|
+
});
|
|
22
|
+
this._index = null;
|
|
23
|
+
this.isInitialized = false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the initialization status
|
|
27
|
+
*/
|
|
28
|
+
getIsInitialized() {
|
|
29
|
+
return this.isInitialized;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get the MeiliSearch client
|
|
33
|
+
*/
|
|
34
|
+
getClient() {
|
|
35
|
+
return this.client;
|
|
36
|
+
}
|
|
37
|
+
get index() {
|
|
38
|
+
return this._index;
|
|
39
|
+
}
|
|
40
|
+
set index(value) {
|
|
41
|
+
this._index = value;
|
|
42
|
+
}
|
|
43
|
+
async initialize() {
|
|
44
|
+
try {
|
|
45
|
+
console.log(`Connecting to MeiliSearch at ${this.host}...`);
|
|
46
|
+
// Check if MeiliSearch is running
|
|
47
|
+
await this.client.health();
|
|
48
|
+
console.log("✅ MeiliSearch is healthy");
|
|
49
|
+
// Create or get the index
|
|
50
|
+
try {
|
|
51
|
+
await this.client.createIndex(this.indexName, { primaryKey: "id" });
|
|
52
|
+
console.log(`✅ Created new index: ${this.indexName}`);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.message.includes("exists")) {
|
|
56
|
+
console.log(`✅ Using existing index: ${this.indexName}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Get the index object
|
|
63
|
+
this.index = await this.client.getIndex(this.indexName);
|
|
64
|
+
// Configure search settings
|
|
65
|
+
await this.configureIndex();
|
|
66
|
+
this.isInitialized = true;
|
|
67
|
+
console.log("✅ Search service initialized successfully");
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error("❌ Failed to initialize search service:", error.message);
|
|
71
|
+
console.log("💡 Make sure MeiliSearch is running on", this.host);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Configure search index settings for optimal MCP package search
|
|
77
|
+
*/
|
|
78
|
+
async configureIndex() {
|
|
79
|
+
try {
|
|
80
|
+
// Configure searchable attributes (ranked by importance)
|
|
81
|
+
if (this.index) {
|
|
82
|
+
await this.index.updateSettings({
|
|
83
|
+
searchableAttributes: [
|
|
84
|
+
"name",
|
|
85
|
+
"packageName",
|
|
86
|
+
"description",
|
|
87
|
+
"tools",
|
|
88
|
+
"category",
|
|
89
|
+
"author",
|
|
90
|
+
"keywords",
|
|
91
|
+
],
|
|
92
|
+
// Configure filterable attributes
|
|
93
|
+
filterableAttributes: ["category", "validated", "author", "hasTools", "popularity"],
|
|
94
|
+
// Configure sortable attributes
|
|
95
|
+
sortableAttributes: ["popularity", "name", "category"],
|
|
96
|
+
// Configure ranking rules for relevance
|
|
97
|
+
rankingRules: [
|
|
98
|
+
"words",
|
|
99
|
+
"typo",
|
|
100
|
+
"proximity",
|
|
101
|
+
"attribute",
|
|
102
|
+
"sort",
|
|
103
|
+
"exactness",
|
|
104
|
+
"popularity:desc",
|
|
105
|
+
],
|
|
106
|
+
// Configure synonyms for better search
|
|
107
|
+
synonyms: {
|
|
108
|
+
ai: ["artificial intelligence", "machine learning", "ml"],
|
|
109
|
+
db: ["database"],
|
|
110
|
+
api: ["rest", "graphql"],
|
|
111
|
+
auth: ["authentication", "authorization"],
|
|
112
|
+
mcp: ["model context protocol"],
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
console.log("✅ Search index configured");
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error("❌ Failed to configure index:", error.message);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Transform package data for search indexing
|
|
125
|
+
*/
|
|
126
|
+
transformPackageForIndex(packageName, packageData) {
|
|
127
|
+
// Extract tools information
|
|
128
|
+
const tools = packageData.tools || {};
|
|
129
|
+
const toolNames = Object.keys(tools);
|
|
130
|
+
const toolDescriptions = Object.values(tools)
|
|
131
|
+
.map((t) => t.description || "")
|
|
132
|
+
.join(" ");
|
|
133
|
+
// Calculate basic popularity score (can be enhanced with real metrics)
|
|
134
|
+
const popularity = this.calculatePopularityScore(packageData);
|
|
135
|
+
// Create a safe ID by encoding special characters
|
|
136
|
+
const safeId = this.createSafeId(packageName);
|
|
137
|
+
return {
|
|
138
|
+
id: safeId,
|
|
139
|
+
name: packageData.name || packageName,
|
|
140
|
+
packageName: packageName,
|
|
141
|
+
description: packageData.description || "",
|
|
142
|
+
category: packageData.category || "uncategorized",
|
|
143
|
+
validated: packageData.validated || false,
|
|
144
|
+
author: this.extractAuthor(packageName),
|
|
145
|
+
tools: `${toolNames.join(" ")} ${toolDescriptions}`,
|
|
146
|
+
toolCount: toolNames.length,
|
|
147
|
+
hasTools: toolNames.length > 0,
|
|
148
|
+
keywords: this.extractKeywords(packageData, packageName),
|
|
149
|
+
popularity: popularity,
|
|
150
|
+
path: packageData.path || "",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Create a safe ID for MeiliSearch (alphanumeric, hyphens, underscores only)
|
|
155
|
+
*/
|
|
156
|
+
createSafeId(packageName) {
|
|
157
|
+
return packageName
|
|
158
|
+
.replace(/@/g, "at-")
|
|
159
|
+
.replace(/\//g, "-")
|
|
160
|
+
.replace(/\./g, "_")
|
|
161
|
+
.replace(/:/g, "-")
|
|
162
|
+
.replace(/[^a-zA-Z0-9\-_]/g, "_")
|
|
163
|
+
.substring(0, 500); // Keep under 511 byte limit
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Calculate a popularity score for ranking
|
|
167
|
+
*/
|
|
168
|
+
calculatePopularityScore(packageData) {
|
|
169
|
+
let score = 0;
|
|
170
|
+
// Boost for validated packages
|
|
171
|
+
if (packageData.validated)
|
|
172
|
+
score += 10;
|
|
173
|
+
// Boost for packages with tools
|
|
174
|
+
const toolCount = Object.keys(packageData.tools || {}).length;
|
|
175
|
+
score += toolCount * 2;
|
|
176
|
+
// Boost for packages with good descriptions
|
|
177
|
+
if (packageData.description && packageData.description.length > 50)
|
|
178
|
+
score += 5;
|
|
179
|
+
return score;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Extract author from package name
|
|
183
|
+
*/
|
|
184
|
+
extractAuthor(packageName) {
|
|
185
|
+
if (packageName.startsWith("@")) {
|
|
186
|
+
return packageName.split("/")[0].substring(1);
|
|
187
|
+
}
|
|
188
|
+
return "";
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Extract relevant keywords from package data
|
|
192
|
+
*/
|
|
193
|
+
extractKeywords(packageData, packageName) {
|
|
194
|
+
const keywords = [];
|
|
195
|
+
// Add category as keyword
|
|
196
|
+
if (packageData.category) {
|
|
197
|
+
keywords.push(packageData.category.replace(/-/g, " "));
|
|
198
|
+
}
|
|
199
|
+
// Extract keywords from description
|
|
200
|
+
if (packageData.description) {
|
|
201
|
+
const desc = packageData.description.toLowerCase();
|
|
202
|
+
const commonKeywords = [
|
|
203
|
+
"api",
|
|
204
|
+
"database",
|
|
205
|
+
"auth",
|
|
206
|
+
"search",
|
|
207
|
+
"ai",
|
|
208
|
+
"ml",
|
|
209
|
+
"tool",
|
|
210
|
+
"server",
|
|
211
|
+
"client",
|
|
212
|
+
];
|
|
213
|
+
commonKeywords.forEach((keyword) => {
|
|
214
|
+
if (desc.includes(keyword))
|
|
215
|
+
keywords.push(keyword);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// Extract from package name
|
|
219
|
+
const nameWords = packageName
|
|
220
|
+
.replace(/[@/\-_]/g, " ")
|
|
221
|
+
.split(" ")
|
|
222
|
+
.filter((w) => w.length > 2);
|
|
223
|
+
keywords.push(...nameWords);
|
|
224
|
+
return [...new Set(keywords)].join(" ");
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Index all packages from the packages-list.json file
|
|
228
|
+
*/
|
|
229
|
+
async indexPackages() {
|
|
230
|
+
if (!this.isInitialized) {
|
|
231
|
+
throw new Error("Search service not initialized. Call initialize() first.");
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
console.log("📥 Loading packages data...");
|
|
235
|
+
const packagesPath = path.join(__dirname, "..", "..", "indexes", "packages-list.json");
|
|
236
|
+
const packagesData = await fs.readFile(packagesPath, "utf8");
|
|
237
|
+
const packages = JSON.parse(packagesData);
|
|
238
|
+
console.log(`📦 Found ${Object.keys(packages).length} packages to index`);
|
|
239
|
+
// Transform packages for indexing
|
|
240
|
+
const documents = Object.entries(packages).map(([packageName, packageData]) => this.transformPackageForIndex(packageName, packageData));
|
|
241
|
+
console.log("🔄 Indexing packages...");
|
|
242
|
+
// Add documents to index in batches
|
|
243
|
+
const batchSize = 1000;
|
|
244
|
+
const batches = [];
|
|
245
|
+
for (let i = 0; i < documents.length; i += batchSize) {
|
|
246
|
+
batches.push(documents.slice(i, i + batchSize));
|
|
247
|
+
}
|
|
248
|
+
for (let i = 0; i < batches.length; i++) {
|
|
249
|
+
const batch = batches[i];
|
|
250
|
+
if (this.index) {
|
|
251
|
+
const task = await this.index.addDocuments(batch);
|
|
252
|
+
console.log(`📝 Indexed batch ${i + 1}/${batches.length} (${batch.length} documents) - Task ID: ${task.taskUid}`);
|
|
253
|
+
// Wait for task completion
|
|
254
|
+
await this.client.waitForTask(task.taskUid);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Get final stats
|
|
258
|
+
if (!this.index) {
|
|
259
|
+
throw new Error("Index is not initialized");
|
|
260
|
+
}
|
|
261
|
+
const stats = await this.index.getStats();
|
|
262
|
+
console.log(`✅ Indexing complete! ${stats.numberOfDocuments} documents indexed`);
|
|
263
|
+
return stats;
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
console.error("❌ Failed to index packages:", error.message);
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Search packages with advanced options
|
|
272
|
+
*/
|
|
273
|
+
async search(query, options = {}) {
|
|
274
|
+
if (!this.isInitialized) {
|
|
275
|
+
throw new Error("Search service not initialized. Call initialize() first.");
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const searchOptions = Object.assign({ limit: options.limit || 20, offset: options.offset || 0, filter: options.filter || undefined, sort: options.sort || ["popularity:desc"], attributesToHighlight: ["name", "description"], attributesToCrop: ["description"], cropLength: 100, highlightPreTag: "<mark>", highlightPostTag: "</mark>", matchingStrategy: options.matchingStrategy || "last" }, options);
|
|
279
|
+
if (!this.index) {
|
|
280
|
+
throw new Error("Index is not initialized");
|
|
281
|
+
}
|
|
282
|
+
const results = await this.index.search(query, searchOptions);
|
|
283
|
+
return {
|
|
284
|
+
hits: results.hits,
|
|
285
|
+
query: results.query,
|
|
286
|
+
processingTimeMs: results.processingTimeMs,
|
|
287
|
+
limit: results.limit,
|
|
288
|
+
offset: results.offset,
|
|
289
|
+
estimatedTotalHits: results.estimatedTotalHits,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
console.error("❌ Search failed:", error.message);
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Get search suggestions/autocomplete
|
|
299
|
+
*/
|
|
300
|
+
async suggest(query, limit = 10) {
|
|
301
|
+
if (!this.isInitialized) {
|
|
302
|
+
throw new Error("Search service not initialized. Call initialize() first.");
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
if (!this.index) {
|
|
306
|
+
throw new Error("Index is not initialized");
|
|
307
|
+
}
|
|
308
|
+
const results = await this.index.search(query, {
|
|
309
|
+
limit: limit,
|
|
310
|
+
attributesToRetrieve: ["name", "packageName", "category"],
|
|
311
|
+
attributesToHighlight: ["name"],
|
|
312
|
+
cropLength: 0,
|
|
313
|
+
});
|
|
314
|
+
return results.hits.map((hit) => {
|
|
315
|
+
var _a;
|
|
316
|
+
return ({
|
|
317
|
+
name: hit.name || "",
|
|
318
|
+
packageName: hit.packageName || "",
|
|
319
|
+
category: hit.category || "",
|
|
320
|
+
highlighted: ((_a = hit._formatted) === null || _a === void 0 ? void 0 : _a.name) || hit.name || "",
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
console.error("❌ Suggestions failed:", error.message);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Get faceted search results (for filters)
|
|
331
|
+
*/
|
|
332
|
+
async getFacets() {
|
|
333
|
+
if (!this.isInitialized) {
|
|
334
|
+
throw new Error("Search service not initialized. Call initialize() first.");
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
if (!this.index) {
|
|
338
|
+
throw new Error("Index is not initialized");
|
|
339
|
+
}
|
|
340
|
+
const results = await this.index.search("", {
|
|
341
|
+
limit: 0,
|
|
342
|
+
facets: ["category", "validated", "author"],
|
|
343
|
+
});
|
|
344
|
+
return results.facetDistribution;
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
console.error("❌ Failed to get facets:", error.message);
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get index statistics
|
|
353
|
+
*/
|
|
354
|
+
async getStats() {
|
|
355
|
+
if (!this.isInitialized) {
|
|
356
|
+
throw new Error("Search service not initialized. Call initialize() first.");
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
if (!this.index) {
|
|
360
|
+
throw new Error("Index is not initialized");
|
|
361
|
+
}
|
|
362
|
+
return await this.index.getStats();
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
console.error("❌ Failed to get stats:", error.message);
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Clear the index
|
|
371
|
+
*/
|
|
372
|
+
async clearIndex() {
|
|
373
|
+
if (!this.isInitialized) {
|
|
374
|
+
throw new Error("Search service not initialized. Call initialize() first.");
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
if (!this.index) {
|
|
378
|
+
throw new Error("Index is not initialized");
|
|
379
|
+
}
|
|
380
|
+
const task = await this.index.deleteAllDocuments();
|
|
381
|
+
await this.client.waitForTask(task.taskUid);
|
|
382
|
+
console.log("✅ Index cleared");
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
console.error("❌ Failed to clear index:", error.message);
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Health check for the search service
|
|
391
|
+
*/
|
|
392
|
+
async healthCheck() {
|
|
393
|
+
try {
|
|
394
|
+
await this.client.health();
|
|
395
|
+
const stats = this.isInitialized ? await this.getStats() : null;
|
|
396
|
+
return {
|
|
397
|
+
status: "healthy",
|
|
398
|
+
host: this.host,
|
|
399
|
+
initialized: this.isInitialized,
|
|
400
|
+
indexName: this.indexName,
|
|
401
|
+
documentCount: (stats === null || stats === void 0 ? void 0 : stats.numberOfDocuments) || 0,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
catch (_error) {
|
|
405
|
+
return {
|
|
406
|
+
status: "unhealthy",
|
|
407
|
+
host: this.host,
|
|
408
|
+
initialized: false,
|
|
409
|
+
indexName: this.indexName,
|
|
410
|
+
documentCount: 0,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Export singleton instance
|
|
416
|
+
const searchService = new SearchService();
|
|
417
|
+
export default searchService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import searchService from "./search-service";
|
|
3
|
+
// Mock the MeiliSearch client
|
|
4
|
+
vi.mock("meilisearch", () => {
|
|
5
|
+
return {
|
|
6
|
+
MeiliSearch: vi.fn().mockImplementation(() => {
|
|
7
|
+
const mockIndex = {
|
|
8
|
+
updateSettings: vi.fn().mockResolvedValue({ taskUid: "settings-task" }),
|
|
9
|
+
addDocuments: vi.fn().mockResolvedValue({ taskUid: "documents-task" }),
|
|
10
|
+
getStats: vi.fn().mockResolvedValue({
|
|
11
|
+
numberOfDocuments: 100,
|
|
12
|
+
isIndexing: false,
|
|
13
|
+
fieldDistribution: {},
|
|
14
|
+
}),
|
|
15
|
+
search: vi.fn().mockResolvedValue({
|
|
16
|
+
hits: [{ id: "1", name: "Test Package" }],
|
|
17
|
+
query: "test",
|
|
18
|
+
processingTimeMs: 10,
|
|
19
|
+
limit: 20,
|
|
20
|
+
offset: 0,
|
|
21
|
+
estimatedTotalHits: 1,
|
|
22
|
+
}),
|
|
23
|
+
getTask: vi.fn().mockResolvedValue({ status: "succeeded" }),
|
|
24
|
+
deleteAllDocuments: vi.fn().mockResolvedValue({ taskUid: "clear-task" }),
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
health: vi.fn().mockResolvedValue({ status: "available" }),
|
|
28
|
+
createIndex: vi.fn().mockResolvedValue({ uid: "test-task" }),
|
|
29
|
+
getIndex: vi.fn().mockResolvedValue(mockIndex),
|
|
30
|
+
index: vi.fn().mockReturnValue(mockIndex),
|
|
31
|
+
waitForTask: vi.fn().mockResolvedValue({ status: "succeeded" }),
|
|
32
|
+
};
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
// Mock fs module
|
|
37
|
+
vi.mock("node:fs/promises", () => {
|
|
38
|
+
return {
|
|
39
|
+
default: {
|
|
40
|
+
readFile: vi.fn().mockResolvedValue(JSON.stringify({
|
|
41
|
+
"@test/package": {
|
|
42
|
+
name: "Test Package",
|
|
43
|
+
description: "A test package",
|
|
44
|
+
},
|
|
45
|
+
})),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
describe("SearchService - MCP Registry Search Service Test", () => {
|
|
50
|
+
it("should initialize search service successfully", async () => {
|
|
51
|
+
const result = await searchService.initialize();
|
|
52
|
+
expect(result).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
it("should transform package data for indexing", () => {
|
|
55
|
+
const packageName = "@test/package";
|
|
56
|
+
const packageData = {
|
|
57
|
+
name: "Test Package",
|
|
58
|
+
description: "A test package",
|
|
59
|
+
category: "testing",
|
|
60
|
+
validated: true,
|
|
61
|
+
tools: {
|
|
62
|
+
"test-tool": { description: "A test tool" },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const result = searchService.transformPackageForIndex(packageName, packageData);
|
|
66
|
+
expect(result).toEqual({
|
|
67
|
+
id: "at-test-package",
|
|
68
|
+
name: "Test Package",
|
|
69
|
+
packageName: "@test/package",
|
|
70
|
+
description: "A test package",
|
|
71
|
+
category: "testing",
|
|
72
|
+
validated: true,
|
|
73
|
+
author: "test",
|
|
74
|
+
tools: "test-tool A test tool",
|
|
75
|
+
toolCount: 1,
|
|
76
|
+
hasTools: true,
|
|
77
|
+
keywords: expect.any(String),
|
|
78
|
+
popularity: expect.any(Number),
|
|
79
|
+
path: "",
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it("should perform search successfully", async () => {
|
|
83
|
+
// First initialize the service
|
|
84
|
+
await searchService.initialize();
|
|
85
|
+
// Mock the search result
|
|
86
|
+
const mockResult = {
|
|
87
|
+
hits: [{ id: "1", name: "Test Package" }],
|
|
88
|
+
query: "test",
|
|
89
|
+
processingTimeMs: 10,
|
|
90
|
+
limit: 20,
|
|
91
|
+
offset: 0,
|
|
92
|
+
estimatedTotalHits: 1,
|
|
93
|
+
};
|
|
94
|
+
if (searchService.index) {
|
|
95
|
+
searchService.index.search = vi.fn().mockResolvedValue(mockResult);
|
|
96
|
+
}
|
|
97
|
+
const result = await searchService.search("test");
|
|
98
|
+
expect(result).toEqual(mockResult);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -47880,4 +47880,4 @@
|
|
|
47880
47880
|
"validated": false,
|
|
47881
47881
|
"tools": {}
|
|
47882
47882
|
}
|
|
47883
|
-
}
|
|
47883
|
+
}
|