pagination fix: OC MemberCollection has NO cursor pageInfo — use offset+totalCount pagination (verified against the live API); the cursor query 400'd, taking the sweep and org endpoints down

This commit is contained in:
inference-bot committed 2026-10-20 23:30:32 -06:00
1 parent 42c664c076
commit e4da4da2c6
1 file changed
+18 -18
+18 -18
View File
@@ -646,26 +646,25 @@ def compute_max_budget(allowance: float, credit_balance: float, team_spend: floa
async def fetch_all_member_nodes() -> list[dict]: async def fetch_all_member_nodes() -> list[dict]:
"""Every member node of the collective, following OC's cursor pagination. """Every member node of the collective, via OC's offset pagination.
The members connection caps at 100 per page: an unpaginated query silently The members connection has NO cursor/pageInfo pagination (older-style API)
hides everyone past the first page — the same failure class as Cloudron's and caps at 100 per page: an unpaginated query silently hides everyone
25-per-page user list. Raises on ANY page failure so callers can apply past the first page — the same failure class as Cloudron's 25-per-page
their own fail-safes; never returns a partial list (a partial member list user list. Raises on ANY page failure so callers can apply their own
would make the sweep deactivate everyone missing from it). fail-safes; never returns a partial list (a partial member list would make
the sweep deactivate everyone missing from it).
""" """
if not OC_PERSONAL_TOKEN: if not OC_PERSONAL_TOKEN:
raise RuntimeError("OC_PERSONAL_TOKEN not set; cannot fetch members") raise RuntimeError("OC_PERSONAL_TOKEN not set; cannot fetch members")
nodes: list[dict] = [] nodes: list[dict] = []
after: str | None = None offset = 0
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
for _ in range(20): # hard cap: 20 pages × 100 = 1000 members for _ in range(20): # hard cap: 20 pages × 100 = 2000 members
after_q = f', after: "{after}"' if after else ""
query = ( query = (
'{ collective(slug: "%s") { members(limit: 100%s) { nodes ' '{ collective(slug: "%s") { members(limit: 100, offset: %d) { '
'{ role tier { name } account { name slug type ... on Individual { email } } } ' "totalCount nodes { role tier { name } account { name slug type ... on Individual { email } } } } } }"
'pageInfo { hasNextPage endCursor } } } }' % (OC_COLLECTIVE_SLUG, offset)
% (OC_COLLECTIVE_SLUG, after_q)
) )
r = await client.post( r = await client.post(
OC_GRAPHQL_URL, OC_GRAPHQL_URL,
@@ -674,12 +673,13 @@ async def fetch_all_member_nodes() -> list[dict]:
) )
if r.status_code != 200: if r.status_code != 200:
raise RuntimeError(f"OC member fetch failed: HTTP {r.status_code}") raise RuntimeError(f"OC member fetch failed: HTTP {r.status_code}")
data = ((r.json().get("data") or {}).get("collective") or {}).get("members") or {} members = ((r.json().get("data") or {}).get("collective") or {}).get("members") or {}
nodes.extend(data.get("nodes") or []) total = members.get("totalCount") or 0
info = data.get("pageInfo") or {} page = members.get("nodes") or []
if not info.get("hasNextPage") or not info.get("endCursor"): nodes.extend(page)
offset += len(page)
if not page or offset >= total:
return nodes return nodes
after = info["endCursor"]
raise RuntimeError("OC member pagination exceeded 20 pages") raise RuntimeError("OC member pagination exceeded 20 pages")