diff --git a/convex/schema.ts b/convex/schema.ts index 4ed178d93..5d71a207f 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -384,7 +384,8 @@ const souls = defineTable({ .index("by_slug", ["slug"]) .index("by_owner", ["ownerUserId"]) .index("by_owner_publisher", ["ownerPublisherId"]) - .index("by_updated", ["updatedAt"]); + .index("by_updated", ["updatedAt"]) + .index("by_active_updated", ["softDeletedAt", "updatedAt"]); const skillVersions = defineTable({ skillId: v.id("skills"), diff --git a/convex/souls.test.ts b/convex/souls.test.ts index 62cabbcc1..94a2a7be2 100644 --- a/convex/souls.test.ts +++ b/convex/souls.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { getSoulBySlugInternal, insertVersion } from "./souls"; +import { getSoulBySlugInternal, insertVersion, list } from "./souls"; type WrappedHandler = { _handler: (ctx: unknown, args: TArgs) => Promise; @@ -10,6 +10,7 @@ const insertVersionHandler = (insertVersion as unknown as WrappedHandler )._handler; +const listHandler = (list as unknown as WrappedHandler<{ ownerUserId?: string; limit?: number }>)._handler; describe("souls.insertVersion", () => { it("throws a soul-specific ownership error for non-owners", async () => { @@ -139,3 +140,75 @@ describe("souls.insertVersion", () => { ); }); }); + +describe("souls.list", () => { + it("uses the active browse index and only takes the requested limit", async () => { + let requestedIndex: string | null = null; + let requestedSoftDeletedAt: number | undefined; + let requestedLimit: number | null = null; + + const result = await listHandler( + { + db: { + query: vi.fn((table: string) => { + if (table !== "souls") throw new Error(`unexpected table ${table}`); + return { + withIndex: ( + name: string, + build: + | ((q: { eq: (field: string, value: undefined) => unknown }) => unknown) + | undefined, + ) => { + requestedIndex = name; + const q = { + eq: (field: string, value: undefined) => { + if (field !== "softDeletedAt") throw new Error(`unexpected field ${field}`); + requestedSoftDeletedAt = value; + return q; + }, + }; + build?.(q); + return { + order: () => ({ + take: async (limit: number) => { + requestedLimit = limit; + return [ + { + _id: "souls:1", + _creationTime: 1, + slug: "demo-soul", + displayName: "Demo Soul", + summary: "A demo soul", + ownerUserId: "users:owner", + ownerPublisherId: undefined, + latestVersionId: undefined, + tags: {}, + softDeletedAt: undefined, + stats: { downloads: 1, stars: 2, versions: 3, comments: 4 }, + createdAt: 1, + updatedAt: 2, + }, + ]; + }, + }), + }; + }, + }; + }), + }, + } as never, + { limit: 7 } as never, + ); + + expect(requestedIndex).toBe("by_active_updated"); + expect(requestedSoftDeletedAt).toBeUndefined(); + expect(requestedLimit).toBe(7); + expect(result).toEqual([ + expect.objectContaining({ + _id: "souls:1", + slug: "demo-soul", + displayName: "Demo Soul", + }), + ]); + }); +}); diff --git a/convex/souls.ts b/convex/souls.ts index c72867223..008513383 100644 --- a/convex/souls.ts +++ b/convex/souls.ts @@ -138,11 +138,10 @@ export const list = query({ } const entries = await ctx.db .query("souls") + .withIndex("by_active_updated", (q) => q.eq("softDeletedAt", undefined)) .order("desc") - .take(limit * 5); + .take(limit); return entries - .filter((soul) => !soul.softDeletedAt) - .slice(0, limit) .map((soul) => toPublicSoul(soul)) .filter((soul): soul is NonNullable => Boolean(soul)); }, diff --git a/src/routes/cli/auth.tsx b/src/routes/cli/auth.tsx index 23dbff321..259cf2f7e 100644 --- a/src/routes/cli/auth.tsx +++ b/src/routes/cli/auth.tsx @@ -61,17 +61,17 @@ export function CliAuth({ navigate = (url: string) => window.location.assign(url hash.set("token", result.token); hash.set("registry", registry); hash.set("state", state); - const callbackUrl = `${redirectUri}#${hash.toString()}`; + const redirectUrl = `${redirectUri}#${hash.toString()}`; // Render the fallback token before attempting navigation so it is // always visible if the browser blocks or fails the http:// redirect // (e.g. ERR_CONNECTION_REFUSED when the CLI server has already shut // down, or Chrome's HTTPS-first mode interfering with localhost). flushSync(() => { setToken(result.token); - setCallbackUrl(callbackUrl); + setCallbackUrl(redirectUrl); setStatus("Redirecting to CLI…"); }); - navigate(callbackUrl); + navigate(redirectUrl); }; void run().catch((error) => {