fix(api): route package search through digest index

This commit is contained in:
Vincent Koc
2026-05-03 02:01:20 -07:00
parent e76b72cdb1
commit cf5778d7d5
2 changed files with 58 additions and 118 deletions
+46 -49
View File
@@ -2821,7 +2821,6 @@ describe("httpApiV1 handlers", () => {
it("packages search forwards executesCode and capabilityTag", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if ("paginationOpts" in args) return { page: [], isDone: true, continueCursor: "" };
if ("query" in args) return [];
return null;
});
@@ -2833,12 +2832,12 @@ describe("httpApiV1 handlers", () => {
),
);
if (response.status !== 200) throw new Error(await response.text());
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect(runQuery.mock.calls.map(([, args]) => args)).toContainEqual(
expect.objectContaining({
query: "test",
limit: 5,
executesCode: true,
capabilityTag: "tools",
paginationOpts: { cursor: null, numItems: 50 },
}),
);
expect(findRateLimitCallArgs(runMutation)).toMatchObject({
@@ -2850,7 +2849,6 @@ describe("httpApiV1 handlers", () => {
it("packages search maps environment filters to capability tags", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if ("paginationOpts" in args) return { page: [], isDone: true, continueCursor: "" };
if ("query" in args) return [];
return null;
});
@@ -2860,8 +2858,7 @@ describe("httpApiV1 handlers", () => {
new Request("https://example.com/api/v1/packages/search?q=test&requiresBrowser=true"),
);
if (response.status !== 200) throw new Error(await response.text());
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect(runQuery.mock.calls.map(([, args]) => args)).toContainEqual(
expect.objectContaining({
capabilityTag: "requires:browser",
}),
@@ -2870,7 +2867,6 @@ describe("httpApiV1 handlers", () => {
it("packages search maps artifact filters to capability tags", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if ("paginationOpts" in args) return { page: [], isDone: true, continueCursor: "" };
if ("query" in args) return [];
return null;
});
@@ -2880,8 +2876,7 @@ describe("httpApiV1 handlers", () => {
new Request("https://example.com/api/v1/packages/search?q=test&artifactKind=npm-pack"),
);
if (response.status !== 200) throw new Error(await response.text());
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect(runQuery.mock.calls.map(([, args]) => args)).toContainEqual(
expect.objectContaining({
capabilityTag: "artifact:npm-pack",
}),
@@ -3044,9 +3039,10 @@ describe("httpApiV1 handlers", () => {
it("plugins search defaults to plugin package families", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (args.family === "code-plugin") {
return {
page: [
{
return [
{
score: 10,
package: {
name: "weather-code",
displayName: "Weather Code",
family: "code-plugin",
@@ -3055,15 +3051,14 @@ describe("httpApiV1 handlers", () => {
createdAt: 10,
updatedAt: 100,
},
],
isDone: true,
continueCursor: "",
};
},
];
}
if (args.family === "bundle-plugin") {
return {
page: [
{
return [
{
score: 10,
package: {
name: "weather-bundle",
displayName: "Weather Bundle",
family: "bundle-plugin",
@@ -3072,10 +3067,8 @@ describe("httpApiV1 handlers", () => {
createdAt: 20,
updatedAt: 200,
},
],
isDone: true,
continueCursor: "",
};
},
];
}
throw new Error(`unexpected family ${String(args.family)}`);
});
@@ -3097,7 +3090,8 @@ describe("httpApiV1 handlers", () => {
for (const [, args] of runQuery.mock.calls) {
expect(args).toEqual(
expect.objectContaining({
paginationOpts: { cursor: null, numItems: 50 },
query: "weather",
limit: 7,
}),
);
}
@@ -3106,24 +3100,31 @@ describe("httpApiV1 handlers", () => {
it("plugins search dedupes and sorts results from both plugin families", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (args.family === "code-plugin") {
return {
page: [
makeCatalogItem("shared-plugin", { family: "code-plugin", updatedAt: 100 }),
makeCatalogItem("plugin-code", { family: "code-plugin", updatedAt: 50 }),
],
isDone: true,
continueCursor: "",
};
return [
{
score: 10,
package: makeCatalogItem("shared-plugin", { family: "code-plugin", updatedAt: 100 }),
},
{
score: 50,
package: makeCatalogItem("plugin-code", { family: "code-plugin", updatedAt: 50 }),
},
];
}
if (args.family === "bundle-plugin") {
return {
page: [
makeCatalogItem("plugin-bundle", { family: "bundle-plugin", updatedAt: 80 }),
makeCatalogItem("shared-plugin", { family: "bundle-plugin", updatedAt: 60 }),
],
isDone: true,
continueCursor: "",
};
return [
{
score: 70,
package: makeCatalogItem("plugin-bundle", { family: "bundle-plugin", updatedAt: 80 }),
},
{
score: 10,
package: makeCatalogItem("shared-plugin", {
family: "bundle-plugin",
updatedAt: 60,
}),
},
];
}
throw new Error(`unexpected family ${String(args.family)}`);
});
@@ -3174,7 +3175,6 @@ describe("httpApiV1 handlers", () => {
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if ("userId" in args) return { _id: args.userId };
if ("paginationOpts" in args) return { page: [], isDone: true, continueCursor: "" };
if ("query" in args) return [];
return null;
});
@@ -3186,12 +3186,11 @@ describe("httpApiV1 handlers", () => {
);
expect(response.status).toBe(200);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect(runQuery.mock.calls.map(([, args]) => args)).toContainEqual(
expect.objectContaining({
query: "secret",
channel: "private",
viewerUserId: "users:owner",
paginationOpts: { cursor: null, numItems: 50 },
}),
);
});
@@ -3223,7 +3222,6 @@ describe("httpApiV1 handlers", () => {
if (query === internal.users.getByIdInternal) {
throw new Error("Table mismatch");
}
if ("paginationOpts" in args) return { page: [], isDone: true, continueCursor: "" };
if ("query" in args) return [];
return null;
});
@@ -3239,12 +3237,11 @@ describe("httpApiV1 handlers", () => {
internal.users.getByIdInternal,
expect.objectContaining({ userId: "users:broken" }),
);
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect(runQuery.mock.calls.map(([, args]) => args)).toContainEqual(
expect.objectContaining({
query: "secret",
channel: "community",
viewerUserId: undefined,
paginationOpts: { cursor: null, numItems: 50 },
}),
);
});
+12 -69
View File
@@ -735,37 +735,6 @@ function compareCatalogItems(a: CatalogListItem, b: CatalogListItem) {
return a.name.localeCompare(b.name);
}
const HTTP_PACKAGE_SEARCH_PAGE_SIZE = 50;
const HTTP_PACKAGE_SEARCH_SCAN_PAGES = 20;
function catalogSearchScore(item: CatalogListItem, queryText: string) {
const needle = queryText.toLowerCase();
const name = item.name.toLowerCase();
const display = item.displayName.toLowerCase();
const runtimeId = item.runtimeId?.toLowerCase() ?? "";
const summary = (item.summary ?? "").toLowerCase();
let score = 0;
if (name === needle) score += 200;
else if (name.startsWith(needle)) score += 120;
else if (name.includes(needle)) score += 80;
if (display === needle) score += 150;
else if (display.startsWith(needle)) score += 70;
else if (display.includes(needle)) score += 40;
if (runtimeId === needle) score += 180;
else if (runtimeId.startsWith(needle)) score += 90;
else if (runtimeId.includes(needle)) score += 45;
if (summary.includes(needle)) score += 20;
if ((item.capabilityTags ?? []).some((entry) => entry.toLowerCase().includes(needle))) {
score += 12;
}
if (item.isOfficial) score += 5;
return score;
}
function compareCatalogSearchEntries(a: CatalogSearchEntry, b: CatalogSearchEntry) {
return (
b.score - a.score ||
@@ -774,7 +743,7 @@ function compareCatalogSearchEntries(a: CatalogSearchEntry, b: CatalogSearchEntr
);
}
async function searchPackageCatalogByListing(
async function searchPackageCatalog(
ctx: ActionCtx,
args: {
query: string;
@@ -788,22 +757,12 @@ async function searchPackageCatalogByListing(
viewerUserId?: Id<"users">;
},
): Promise<CatalogSearchEntry[]> {
const queryText = args.query.trim().toLowerCase();
if (!queryText) return [];
const matches: CatalogSearchEntry[] = [];
const seen = new Set<string>();
let cursor: string | null = null;
let done = false;
let loops = 0;
while (!done && loops < HTTP_PACKAGE_SEARCH_SCAN_PAGES) {
loops += 1;
const result: {
page: CatalogListItem[];
isDone: boolean;
continueCursor: string | null;
} = await runQueryRef(ctx, internalRefs.packages.listPageForViewerInternal, {
return await runQueryRef<CatalogSearchEntry[]>(
ctx,
internalRefs.packages.searchForViewerInternal,
{
query: args.query,
limit: args.limit,
family: args.family,
channel: args.channel,
isOfficial: args.isOfficial,
@@ -811,24 +770,8 @@ async function searchPackageCatalogByListing(
executesCode: args.executesCode,
capabilityTag: args.capabilityTag,
viewerUserId: args.viewerUserId,
paginationOpts: { cursor, numItems: HTTP_PACKAGE_SEARCH_PAGE_SIZE },
});
for (const item of result.page) {
const key = `${item.family}:${item.name}`;
if (seen.has(key)) continue;
const score = catalogSearchScore(item, queryText);
if (score <= 0) continue;
seen.add(key);
matches.push({ score, package: item });
}
done = result.isDone;
cursor = result.continueCursor;
if (!cursor && !done) break;
}
return matches.sort(compareCatalogSearchEntries).slice(0, args.limit);
},
);
}
async function resolveSkillTags(
@@ -2027,7 +1970,7 @@ async function searchPackages(
if (!family && options?.pluginFamilies?.length) {
const pluginResults = await Promise.all(
options.pluginFamilies.map((pluginFamily) =>
searchPackageCatalogByListing(ctx, {
searchPackageCatalog(ctx, {
query: queryText,
limit,
family: pluginFamily,
@@ -2052,7 +1995,7 @@ async function searchPackages(
.sort(compareCatalogSearchEntries)
.slice(0, limit);
} else {
results = await searchPackageCatalogByListing(ctx, {
results = await searchPackageCatalog(ctx, {
query: queryText,
limit,
family,
@@ -2066,7 +2009,7 @@ async function searchPackages(
}
} else {
const [packageResults, skillResults] = await Promise.all([
searchPackageCatalogByListing(ctx, {
searchPackageCatalog(ctx, {
query: queryText,
limit,
channel,