mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix(web): recall publishers outside browse window in search (#2790)
* fix: recall publishers outside top install window in search Publisher search only scanned the top 500 by installs and dropped empty profiles, so handles like vincentkoc never appeared even when the user profile was public. * test: cover publisher search recall for low-install handles Add regression coverage for publishers with published skills that fall outside the top install browse window, matching the vyctorbrzezowski case. * test: align publisher search mocks with downloads browse indexes * chore: add production publisher search proof for PR 2790 * test: drop invalid publisher list stats assertion Remove stats.skills expectation from listPublicPage search recall test; public list items only expose downloads and installs counts. * chore: retrigger CI after delete-account flake
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
# ClawHub publisher search proof
|
||||||
|
|
||||||
|
Status: pass (production bug reproduction before backend deploy)
|
||||||
|
|
||||||
|
## Convex read-only validation (`wry-manatee-359.convex.cloud`)
|
||||||
|
|
||||||
|
| Query | `listPublicPage` handles |
|
||||||
|
| --- | --- |
|
||||||
|
| `vyctor` | `[]` |
|
||||||
|
| `vyctorbrzezowski` | `[]` |
|
||||||
|
| `vincent` | `["vincentchan"]` |
|
||||||
|
| `vincentkoc` | `[]` |
|
||||||
|
|
||||||
|
## Profiles that exist but are missing from search
|
||||||
|
|
||||||
|
- `vyctorbrzezowski` → 5 skills, 1 package, 46 installs
|
||||||
|
- `vincentkoc` → public profile, 0 published skills
|
||||||
|
|
||||||
|
## Unit tests
|
||||||
|
|
||||||
|
`VITE_CONVEX_URL=https://example.invalid bunx vitest run convex/publishers.test.ts`
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"baseline": "production",
|
||||||
|
"candidate": "production-before-fix",
|
||||||
|
"generatedAt": "2026-06-23T02:50:00.000Z",
|
||||||
|
"mode": "feature",
|
||||||
|
"status": "pass",
|
||||||
|
"lanes": [
|
||||||
|
{
|
||||||
|
"name": "candidate",
|
||||||
|
"ref": "https://clawhub.ai",
|
||||||
|
"status": "pass",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"lane": "candidate",
|
||||||
|
"name": "publishers?q=vyctorbrzezowski returns no publishers (prod before deploy)",
|
||||||
|
"screenshot": "screenshots/vyctorbrzezowski-empty.png",
|
||||||
|
"slug": "vyctorbrzezowski-empty",
|
||||||
|
"status": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lane": "candidate",
|
||||||
|
"name": "publishers?q=vincent shows vincentchan but not vincentkoc (prod before deploy)",
|
||||||
|
"screenshot": "screenshots/vincent-missing-vincentkoc.png",
|
||||||
|
"slug": "vincent-missing-vincentkoc",
|
||||||
|
"status": "pass"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+282
-3
@@ -1542,11 +1542,20 @@ describe("publishers membership controls", () => {
|
|||||||
query: vi.fn((table: string) => ({
|
query: vi.fn((table: string) => ({
|
||||||
withIndex: vi.fn((indexName: string, buildQuery: (q: unknown) => unknown) => {
|
withIndex: vi.fn((indexName: string, buildQuery: (q: unknown) => unknown) => {
|
||||||
const fields: Record<string, unknown> = {};
|
const fields: Record<string, unknown> = {};
|
||||||
|
const range: Record<string, unknown> = {};
|
||||||
const q = {
|
const q = {
|
||||||
eq: (field: string, value: unknown) => {
|
eq: (field: string, value: unknown) => {
|
||||||
fields[field] = value;
|
fields[field] = value;
|
||||||
return q;
|
return q;
|
||||||
},
|
},
|
||||||
|
gte: (field: string, value: unknown) => {
|
||||||
|
range.gte = { field, value };
|
||||||
|
return q;
|
||||||
|
},
|
||||||
|
lt: (field: string, value: unknown) => {
|
||||||
|
range.lt = { field, value };
|
||||||
|
return q;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
buildQuery(q);
|
buildQuery(q);
|
||||||
if (table === "publishers" && indexName === "by_handle") {
|
if (table === "publishers" && indexName === "by_handle") {
|
||||||
@@ -1590,9 +1599,13 @@ describe("publishers membership controls", () => {
|
|||||||
}
|
}
|
||||||
if (table === "publishers" && indexName === "by_active_kind_handle") {
|
if (table === "publishers" && indexName === "by_active_kind_handle") {
|
||||||
return {
|
return {
|
||||||
collect: vi.fn(async () =>
|
take: vi.fn(async () => {
|
||||||
publisherRows.filter((publisher) => publisher.kind === fields.kind),
|
const prefix = (range.gte as { value: string } | undefined)?.value ?? "";
|
||||||
),
|
return publisherRows.filter(
|
||||||
|
(publisher) =>
|
||||||
|
publisher.kind === fields.kind && publisher.handle.startsWith(prefix),
|
||||||
|
);
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -1621,6 +1634,255 @@ describe("publishers membership controls", () => {
|
|||||||
expect(result.page.map((item) => item.handle)).toEqual(["alice"]);
|
expect(result.page.map((item) => item.handle)).toEqual(["alice"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("finds publishers outside the popular install window via handle prefix search", async () => {
|
||||||
|
const popularRows = Array.from({ length: 500 }, (_, index) => ({
|
||||||
|
_id: `publishers:popular-${index}`,
|
||||||
|
_creationTime: index,
|
||||||
|
kind: "user" as const,
|
||||||
|
handle: `popular-${index}`,
|
||||||
|
displayName: `Popular ${index}`,
|
||||||
|
linkedUserId: `users:popular-${index}`,
|
||||||
|
publishedSkills: 1,
|
||||||
|
publishedPackages: 0,
|
||||||
|
totalInstalls: 500 - index,
|
||||||
|
totalDownloads: 500 - index,
|
||||||
|
totalStars: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
}));
|
||||||
|
const vincentkoc = {
|
||||||
|
_id: "publishers:vincentkoc",
|
||||||
|
_creationTime: 1,
|
||||||
|
kind: "user" as const,
|
||||||
|
handle: "vincentkoc",
|
||||||
|
displayName: "Vincent Koc",
|
||||||
|
linkedUserId: "users:vincentkoc",
|
||||||
|
publishedSkills: 0,
|
||||||
|
publishedPackages: 0,
|
||||||
|
totalInstalls: 0,
|
||||||
|
totalDownloads: 0,
|
||||||
|
totalStars: 0,
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
};
|
||||||
|
const ctx = {
|
||||||
|
db: {
|
||||||
|
get: vi.fn(async (id: string) => {
|
||||||
|
if (id === "users:vincentkoc") {
|
||||||
|
return { _id: id, image: "https://github.com/vincentkoc.png" };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
query: vi.fn((table: string) => ({
|
||||||
|
withIndex: vi.fn((indexName: string, buildQuery: (q: unknown) => unknown) => {
|
||||||
|
const fields: Record<string, unknown> = {};
|
||||||
|
const range: Record<string, unknown> = {};
|
||||||
|
const q = {
|
||||||
|
eq: (field: string, value: unknown) => {
|
||||||
|
fields[field] = value;
|
||||||
|
return q;
|
||||||
|
},
|
||||||
|
gte: (field: string, value: unknown) => {
|
||||||
|
range.gte = { field, value };
|
||||||
|
return q;
|
||||||
|
},
|
||||||
|
lt: (field: string, value: unknown) => {
|
||||||
|
range.lt = { field, value };
|
||||||
|
return q;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
buildQuery(q);
|
||||||
|
if (table === "publishers" && indexName === "by_handle") {
|
||||||
|
return {
|
||||||
|
unique: vi.fn(async () =>
|
||||||
|
fields.handle === "vincent"
|
||||||
|
? null
|
||||||
|
: fields.handle === "vincentkoc"
|
||||||
|
? vincentkoc
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === "publishers" && indexName === "by_active_total_downloads") {
|
||||||
|
return {
|
||||||
|
order: vi.fn(() => ({
|
||||||
|
take: vi.fn(async () => popularRows),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === "publishers" && indexName === "by_active_total_installs") {
|
||||||
|
return {
|
||||||
|
order: vi.fn(() => ({
|
||||||
|
take: vi.fn(async () => popularRows),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === "publishers" && indexName === "by_active_kind_handle") {
|
||||||
|
return {
|
||||||
|
take: vi.fn(async () => {
|
||||||
|
const prefix = (range.gte as { value: string } | undefined)?.value ?? "";
|
||||||
|
const upper = (range.lt as { value: string } | undefined)?.value ?? "";
|
||||||
|
if (fields.kind === "user" && prefix === "vincent" && upper === "vincent\uffff") {
|
||||||
|
return [vincentkoc];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(table === "skills" || table === "packages") &&
|
||||||
|
indexName === "by_owner_publisher_active_downloads"
|
||||||
|
) {
|
||||||
|
return indexedRows([]);
|
||||||
|
}
|
||||||
|
if (table === "officialPublishers" && indexName === "by_publisher") {
|
||||||
|
return { unique: vi.fn(async () => null) };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected ${table} index ${indexName}`);
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await listPublicPageHandler(ctx as never, {
|
||||||
|
query: "vincent",
|
||||||
|
paginationOpts: { cursor: null, numItems: 25 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.page.map((item) => item.handle)).toEqual(["vincentkoc"]);
|
||||||
|
expect(result.counts).toEqual({ all: 1, individuals: 1, organizations: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds publishers with published skills outside the popular install window", async () => {
|
||||||
|
const popularRows = Array.from({ length: 500 }, (_, index) => ({
|
||||||
|
_id: `publishers:popular-${index}`,
|
||||||
|
_creationTime: index,
|
||||||
|
kind: "user" as const,
|
||||||
|
handle: `popular-${index}`,
|
||||||
|
displayName: `Popular ${index}`,
|
||||||
|
linkedUserId: `users:popular-${index}`,
|
||||||
|
publishedSkills: 1,
|
||||||
|
publishedPackages: 0,
|
||||||
|
totalInstalls: 500 - index,
|
||||||
|
totalDownloads: 500 - index,
|
||||||
|
totalStars: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
}));
|
||||||
|
const vyctorbrzezowski = {
|
||||||
|
_id: "publishers:vyctorbrzezowski",
|
||||||
|
_creationTime: 1,
|
||||||
|
kind: "user" as const,
|
||||||
|
handle: "vyctorbrzezowski",
|
||||||
|
displayName: "Vyctor Brzezowski",
|
||||||
|
linkedUserId: "users:vyctorbrzezowski",
|
||||||
|
publishedSkills: 5,
|
||||||
|
publishedPackages: 1,
|
||||||
|
totalInstalls: 46,
|
||||||
|
totalDownloads: 1288,
|
||||||
|
totalStars: 0,
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
};
|
||||||
|
const ctx = {
|
||||||
|
db: {
|
||||||
|
get: vi.fn(async (id: string) => {
|
||||||
|
if (id === "users:vyctorbrzezowski") {
|
||||||
|
return { _id: id, image: "https://github.com/vyctorbrzezowski.png" };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
query: vi.fn((table: string) => ({
|
||||||
|
withIndex: vi.fn((indexName: string, buildQuery: (q: unknown) => unknown) => {
|
||||||
|
const fields: Record<string, unknown> = {};
|
||||||
|
const range: Record<string, unknown> = {};
|
||||||
|
const q = {
|
||||||
|
eq: (field: string, value: unknown) => {
|
||||||
|
fields[field] = value;
|
||||||
|
return q;
|
||||||
|
},
|
||||||
|
gte: (field: string, value: unknown) => {
|
||||||
|
range.gte = { field, value };
|
||||||
|
return q;
|
||||||
|
},
|
||||||
|
lt: (field: string, value: unknown) => {
|
||||||
|
range.lt = { field, value };
|
||||||
|
return q;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
buildQuery(q);
|
||||||
|
if (table === "publishers" && indexName === "by_handle") {
|
||||||
|
return {
|
||||||
|
unique: vi.fn(async () =>
|
||||||
|
fields.handle === "vyctorbrzezowski" ? vyctorbrzezowski : null,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === "publishers" && indexName === "by_active_total_downloads") {
|
||||||
|
return {
|
||||||
|
order: vi.fn(() => ({
|
||||||
|
take: vi.fn(async () => popularRows),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === "publishers" && indexName === "by_active_total_installs") {
|
||||||
|
return {
|
||||||
|
order: vi.fn(() => ({
|
||||||
|
take: vi.fn(async () => popularRows),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === "publishers" && indexName === "by_active_kind_handle") {
|
||||||
|
return {
|
||||||
|
take: vi.fn(async () => {
|
||||||
|
const prefix = (range.gte as { value: string } | undefined)?.value ?? "";
|
||||||
|
const upper = (range.lt as { value: string } | undefined)?.value ?? "";
|
||||||
|
if (fields.kind === "user" && prefix === "vyctor" && upper === "vyctor\uffff") {
|
||||||
|
return [vyctorbrzezowski];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === "skills" && indexName === "by_owner_publisher_active_downloads") {
|
||||||
|
return indexedRows([
|
||||||
|
{
|
||||||
|
_id: "skills:vyctor-demo",
|
||||||
|
ownerPublisherId: "publishers:vyctorbrzezowski",
|
||||||
|
softDeletedAt: undefined,
|
||||||
|
displayName: "Demo Skill",
|
||||||
|
statsInstallsAllTime: 46,
|
||||||
|
statsDownloads: 1288,
|
||||||
|
statsStars: 0,
|
||||||
|
updatedAt: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (table === "packages" && indexName === "by_owner_publisher_active_downloads") {
|
||||||
|
return indexedRows([]);
|
||||||
|
}
|
||||||
|
if (table === "officialPublishers" && indexName === "by_publisher") {
|
||||||
|
return { unique: vi.fn(async () => null) };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected ${table} index ${indexName}`);
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefixResult = await listPublicPageHandler(ctx as never, {
|
||||||
|
query: "vyctor",
|
||||||
|
paginationOpts: { cursor: null, numItems: 25 },
|
||||||
|
});
|
||||||
|
const exactResult = await listPublicPageHandler(ctx as never, {
|
||||||
|
query: "vyctorbrzezowski",
|
||||||
|
paginationOpts: { cursor: null, numItems: 25 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prefixResult.page.map((item) => item.handle)).toEqual(["vyctorbrzezowski"]);
|
||||||
|
expect(exactResult.page.map((item) => item.handle)).toEqual(["vyctorbrzezowski"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("filters hidden legacy user publishers before counting and paginating public publisher pages", async () => {
|
it("filters hidden legacy user publishers before counting and paginating public publisher pages", async () => {
|
||||||
const publisherRows = [
|
const publisherRows = [
|
||||||
{
|
{
|
||||||
@@ -2261,11 +2523,20 @@ describe("publishers membership controls", () => {
|
|||||||
query: vi.fn((table: string) => ({
|
query: vi.fn((table: string) => ({
|
||||||
withIndex: vi.fn((indexName: string, buildQuery: (q: unknown) => unknown) => {
|
withIndex: vi.fn((indexName: string, buildQuery: (q: unknown) => unknown) => {
|
||||||
const fields: Record<string, unknown> = {};
|
const fields: Record<string, unknown> = {};
|
||||||
|
const range: Record<string, unknown> = {};
|
||||||
const q = {
|
const q = {
|
||||||
eq: (field: string, value: unknown) => {
|
eq: (field: string, value: unknown) => {
|
||||||
fields[field] = value;
|
fields[field] = value;
|
||||||
return q;
|
return q;
|
||||||
},
|
},
|
||||||
|
gte: (field: string, value: unknown) => {
|
||||||
|
range.gte = { field, value };
|
||||||
|
return q;
|
||||||
|
},
|
||||||
|
lt: (field: string, value: unknown) => {
|
||||||
|
range.lt = { field, value };
|
||||||
|
return q;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
buildQuery(q);
|
buildQuery(q);
|
||||||
if (table === "publishers" && indexName === "by_active_total_downloads") {
|
if (table === "publishers" && indexName === "by_active_total_downloads") {
|
||||||
@@ -2276,6 +2547,9 @@ describe("publishers membership controls", () => {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (table === "publishers" && indexName === "by_handle") {
|
||||||
|
return { unique: vi.fn(async () => null) };
|
||||||
|
}
|
||||||
if (table === "publishers" && indexName === "by_active_total_installs") {
|
if (table === "publishers" && indexName === "by_active_total_installs") {
|
||||||
return {
|
return {
|
||||||
order: vi.fn(() => ({
|
order: vi.fn(() => ({
|
||||||
@@ -2283,6 +2557,11 @@ describe("publishers membership controls", () => {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (table === "publishers" && indexName === "by_active_kind_handle") {
|
||||||
|
return {
|
||||||
|
take: vi.fn(async () => []),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
(table === "skills" || table === "packages") &&
|
(table === "skills" || table === "packages") &&
|
||||||
indexName === "by_owner_publisher_active_downloads"
|
indexName === "by_owner_publisher_active_downloads"
|
||||||
|
|||||||
+82
-4
@@ -45,6 +45,7 @@ import { adjustUserSkillStatsForSkillChange } from "./lib/userSkillStats";
|
|||||||
|
|
||||||
const MAX_PUBLIC_PUBLISHER_LIST_LIMIT = 500;
|
const MAX_PUBLIC_PUBLISHER_LIST_LIMIT = 500;
|
||||||
const LEGACY_PUBLISHER_DOWNLOAD_FALLBACK_LIMIT = MAX_PUBLIC_PUBLISHER_LIST_LIMIT;
|
const LEGACY_PUBLISHER_DOWNLOAD_FALLBACK_LIMIT = MAX_PUBLIC_PUBLISHER_LIST_LIMIT;
|
||||||
|
const MAX_PUBLISHER_HANDLE_PREFIX_CANDIDATES = 100;
|
||||||
const PUBLISHER_LIST_PREVIEW_LIMIT = 3;
|
const PUBLISHER_LIST_PREVIEW_LIMIT = 3;
|
||||||
const GITHUB_AUTH_ACCOUNT_RECOVERY_MATCH_LIMIT = 10;
|
const GITHUB_AUTH_ACCOUNT_RECOVERY_MATCH_LIMIT = 10;
|
||||||
const PERSONAL_PUBLISHER_RECOVERY_OWNER_MIGRATION_LIMIT = 100;
|
const PERSONAL_PUBLISHER_RECOVERY_OWNER_MIGRATION_LIMIT = 100;
|
||||||
@@ -627,21 +628,30 @@ async function getActivePublisherRowsByDownloads(
|
|||||||
return mergePublisherRows(rankedRows, legacyRows);
|
return mergePublisherRows(rankedRows, legacyRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldIncludePublisherListSummary(
|
||||||
|
summary: PublisherListSummary,
|
||||||
|
options?: { includeEmptyPublishers?: boolean },
|
||||||
|
) {
|
||||||
|
return options?.includeEmptyPublishers || hasPublisherListContent(summary);
|
||||||
|
}
|
||||||
|
|
||||||
async function getVisiblePublisherListSummaries(
|
async function getVisiblePublisherListSummaries(
|
||||||
ctx: Pick<QueryCtx, "db">,
|
ctx: Pick<QueryCtx, "db">,
|
||||||
publishers: Doc<"publishers">[],
|
publishers: Doc<"publishers">[],
|
||||||
|
options?: { includeEmptyPublishers?: boolean },
|
||||||
) {
|
) {
|
||||||
const summaries = await Promise.all(
|
const summaries = await Promise.all(
|
||||||
publishers.map((publisher) => toVisiblePublisherListSummary(ctx, publisher)),
|
publishers.map((publisher) => toVisiblePublisherListSummary(ctx, publisher)),
|
||||||
);
|
);
|
||||||
return summaries
|
return summaries
|
||||||
.filter((summary): summary is PublisherListSummary => Boolean(summary))
|
.filter((summary): summary is PublisherListSummary => Boolean(summary))
|
||||||
.filter(hasPublisherListContent);
|
.filter((summary) => shouldIncludePublisherListSummary(summary, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hydratePublisherListSummaries(
|
async function hydratePublisherListSummaries(
|
||||||
ctx: Pick<QueryCtx, "db">,
|
ctx: Pick<QueryCtx, "db">,
|
||||||
summaries: PublisherListSummary[],
|
summaries: PublisherListSummary[],
|
||||||
|
options?: { includeEmptyPublishers?: boolean },
|
||||||
) {
|
) {
|
||||||
const items = await Promise.all(
|
const items = await Promise.all(
|
||||||
summaries.map((summary) =>
|
summaries.map((summary) =>
|
||||||
@@ -653,7 +663,9 @@ async function hydratePublisherListSummaries(
|
|||||||
);
|
);
|
||||||
return items
|
return items
|
||||||
.filter((item): item is PublisherListItem => Boolean(item))
|
.filter((item): item is PublisherListItem => Boolean(item))
|
||||||
.filter((item) => item.stats.skills + item.stats.packages > 0);
|
.filter(
|
||||||
|
(item) => options?.includeEmptyPublishers || item.stats.skills + item.stats.packages > 0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserStarredCount(ctx: Pick<QueryCtx, "db">, userId: Id<"users">) {
|
async function getUserStarredCount(ctx: Pick<QueryCtx, "db">, userId: Id<"users">) {
|
||||||
@@ -741,6 +753,61 @@ function resolvePublisherDisplayName(
|
|||||||
return linkedUser?.displayName?.trim() || linkedUser?.name?.trim() || publisher.displayName;
|
return linkedUser?.displayName?.trim() || linkedUser?.name?.trim() || publisher.displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publisherHandlePrefixUpperBound(value: string) {
|
||||||
|
return `${value}\uffff`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryActivePublishersByHandlePrefix(
|
||||||
|
ctx: Pick<QueryCtx, "db">,
|
||||||
|
kind: PublicPublisherKindFilter,
|
||||||
|
handlePrefix: string,
|
||||||
|
) {
|
||||||
|
return await ctx.db
|
||||||
|
.query("publishers")
|
||||||
|
.withIndex("by_active_kind_handle", (q) =>
|
||||||
|
q
|
||||||
|
.eq("deletedAt", undefined)
|
||||||
|
.eq("deactivatedAt", undefined)
|
||||||
|
.eq("kind", kind)
|
||||||
|
.gte("handle", handlePrefix)
|
||||||
|
.lt("handle", publisherHandlePrefixUpperBound(handlePrefix)),
|
||||||
|
)
|
||||||
|
.take(MAX_PUBLISHER_HANDLE_PREFIX_CANDIDATES);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectActivePublisherRowsForListPage(
|
||||||
|
ctx: Pick<QueryCtx, "db">,
|
||||||
|
args: {
|
||||||
|
kindFilter?: PublicPublisherKindFilter;
|
||||||
|
queryText?: string;
|
||||||
|
browseRows?: Doc<"publishers">[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const browseRows =
|
||||||
|
args.browseRows ?? (await getActivePublisherRowsByDownloads(ctx, args.kindFilter));
|
||||||
|
const normalizedQuery = args.queryText ? normalizePublisherHandle(args.queryText) : undefined;
|
||||||
|
if (!normalizedQuery) return browseRows;
|
||||||
|
|
||||||
|
const kinds: PublicPublisherKindFilter[] = args.kindFilter ? [args.kindFilter] : ["user", "org"];
|
||||||
|
const [exactMatch, ...prefixMatches] = await Promise.all([
|
||||||
|
getPublisherByHandle(ctx, normalizedQuery),
|
||||||
|
...kinds.map((kind) => queryActivePublishersByHandlePrefix(ctx, kind, normalizedQuery)),
|
||||||
|
]);
|
||||||
|
const merged = new Map<Id<"publishers">, Doc<"publishers">>();
|
||||||
|
for (const publisher of browseRows) {
|
||||||
|
merged.set(publisher._id, publisher);
|
||||||
|
}
|
||||||
|
if (exactMatch && isPublisherActive(exactMatch)) {
|
||||||
|
merged.set(exactMatch._id, exactMatch);
|
||||||
|
}
|
||||||
|
for (const rows of prefixMatches) {
|
||||||
|
for (const publisher of rows) {
|
||||||
|
merged.set(publisher._id, publisher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...merged.values()];
|
||||||
|
}
|
||||||
|
|
||||||
function getPublisherListCounts(items: PublisherListItem[]): PublisherListCounts {
|
function getPublisherListCounts(items: PublisherListItem[]): PublisherListCounts {
|
||||||
const individualCount = items.filter((publisher) => publisher.kind === "user").length;
|
const individualCount = items.filter((publisher) => publisher.kind === "user").length;
|
||||||
const organizationCount = items.filter((publisher) => publisher.kind === "org").length;
|
const organizationCount = items.filter((publisher) => publisher.kind === "org").length;
|
||||||
@@ -2234,10 +2301,20 @@ export const listPublicPage = query({
|
|||||||
Boolean(publisher && !publisher.deletedAt && !publisher.deactivatedAt),
|
Boolean(publisher && !publisher.deletedAt && !publisher.deactivatedAt),
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
const activeRows = officialRows
|
const browseRows = officialRows
|
||||||
? officialRows.filter((publisher) => !kindFilter || publisher.kind === kindFilter)
|
? officialRows.filter((publisher) => !kindFilter || publisher.kind === kindFilter)
|
||||||
: await getActivePublisherRowsByDownloads(ctx, kindFilter);
|
: await getActivePublisherRowsByDownloads(ctx, kindFilter);
|
||||||
const publisherSummaries = await getVisiblePublisherListSummaries(ctx, activeRows);
|
const activeRows = queryText
|
||||||
|
? await collectActivePublisherRowsForListPage(ctx, {
|
||||||
|
kindFilter,
|
||||||
|
queryText,
|
||||||
|
browseRows,
|
||||||
|
})
|
||||||
|
: browseRows;
|
||||||
|
const includeEmptyPublishers = Boolean(queryText);
|
||||||
|
const publisherSummaries = await getVisiblePublisherListSummaries(ctx, activeRows, {
|
||||||
|
includeEmptyPublishers,
|
||||||
|
});
|
||||||
const itemSummaries = publisherSummaries
|
const itemSummaries = publisherSummaries
|
||||||
.filter(
|
.filter(
|
||||||
(summary) =>
|
(summary) =>
|
||||||
@@ -2256,6 +2333,7 @@ export const listPublicPage = query({
|
|||||||
const page = await hydratePublisherListSummaries(
|
const page = await hydratePublisherListSummaries(
|
||||||
ctx,
|
ctx,
|
||||||
itemSummaries.slice(safeOffset, nextOffset),
|
itemSummaries.slice(safeOffset, nextOffset),
|
||||||
|
{ includeEmptyPublishers },
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user