diff --git a/src/app/api/indexer/__tests__/route.test.ts b/src/app/api/indexer/__tests__/route.test.ts index 5a3213bc..08175822 100644 --- a/src/app/api/indexer/__tests__/route.test.ts +++ b/src/app/api/indexer/__tests__/route.test.ts @@ -321,6 +321,22 @@ describe("/api/indexer trust boundary", () => { expect(mockFetch.mock.calls[0][0]).toBe("https://hyperindex.example/graphql") }) + it("routes ProfileCount to Hyperindex with the Hyperindex label-filter query", async () => { + const res = await postIndexer({ + operationName: "ProfileCount", + variables: {}, + }) + expect(res.headers.get("x-indexer-backend")).toBe("hyperindex") + expect(mockFetch.mock.calls[0][0]).toBe("https://hyperindex.example/graphql") + const body = JSON.parse((mockFetch.mock.calls[0][1] as RequestInit).body as string) + expect(body.operationName).toBe("ProfileCount") + expect(body.query).toContain("query ProfileCount") + expect(body.query).toContain("authorLabels") + expect(body.query).toContain("none") + expect(body.query).toContain("activeOnly") + expect(body.query).not.toContain("excludeAuthorLabels") + }) + it("forwards the server-held query string for the requested operation", async () => { await postIndexer({ operationName: "Followers", diff --git a/src/app/api/indexer/route.ts b/src/app/api/indexer/route.ts index 08c18284..7bff5ae8 100644 --- a/src/app/api/indexer/route.ts +++ b/src/app/api/indexer/route.ts @@ -56,11 +56,13 @@ const MAGIC_INDEXER_URL = DEFAULT_MAGIC_INDEXER_URL const HYPERINDEX_URL = process.env.HYPERINDEX_URL || DEFAULT_HYPERINDEX_URL -// Stage migration switchboard. Keep this empty until a stage explicitly -// ports an operation group. Local/staging can also set -// `HYPERINDEX_OPERATIONS=ProfileCount,ActivityCount` to test routing +// Stage migration switchboard. Stacked Stage 1 starts with ProfileCount only; +// all other operations remain Magic-backed until their own PR migrates them. +// Local/staging can also set `HYPERINDEX_OPERATIONS=...` to test routing // without changing production defaults. -const OPERATION_BACKENDS: Partial> = {} +const OPERATION_BACKENDS: Partial> = { + ProfileCount: "hyperindex", +} const HYPERINDEX_OPERATION_NAMES = new Set( (process.env.HYPERINDEX_OPERATIONS || "") .split(",") @@ -77,6 +79,16 @@ function endpointForBackend(backend: IndexerBackend): string { return backend === "hyperindex" ? HYPERINDEX_URL : MAGIC_INDEXER_URL } +function queryForOperation( + operationName: string, + backend: IndexerBackend, +): string | null { + if (backend === "hyperindex") { + return HYPERINDEX_OPERATION_QUERIES[operationName] ?? OPERATIONS[operationName] ?? null + } + return OPERATIONS[operationName] ?? null +} + function logIndexerBackend( operationName: string, backend: IndexerBackend, @@ -200,6 +212,32 @@ const ACTIVITY_NODE_SELECTION = ` * NOTE on adding ops: a new entry MUST come with a `buildVariables` * branch below or the request 400s on unknown variables. */ +const HYPERINDEX_OPERATION_QUERIES: Partial> = { + // Hyperindex does not support Magic's connection-level + // `excludeAuthorLabels` argument. Use the typed label predicate instead: + // hide profiles whose author/account DID has an active likely-test label, + // while letting unlabeled profiles pass through. + ProfileCount: ` + query ProfileCount { + appCertifiedActorProfile( + first: 1 + where: { + authorLabels: { + none: { + val: { in: ${JSON.stringify([...DEFAULT_HIDDEN_ORG_LABELS])} } + activeOnly: true + } + } + } + ) { + totalCount + edges { node { uri } } + pageInfo { hasNextPage } + } + } + `, +} + const OPERATIONS: Record = { // Global activity feed + per-label / per-author filters. Activities: ` @@ -1800,8 +1838,7 @@ export async function POST(request: NextRequest) { } const operationName = parsed.operationName - const query = OPERATIONS[operationName] - if (!query) { + if (!OPERATIONS[operationName]) { return NextResponse.json({ error: "Unknown operation" }, { status: 400 }) } @@ -1819,6 +1856,10 @@ export async function POST(request: NextRequest) { const backend = backendForOperation(operationName) const upstreamUrl = endpointForBackend(backend) + const query = queryForOperation(operationName, backend) + if (!query) { + return NextResponse.json({ error: "Unknown operation" }, { status: 400 }) + } logIndexerBackend(operationName, backend, upstreamUrl) const timeoutController = new AbortController()