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]:
"""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
hides everyone past the first page — the same failure class as Cloudron's
25-per-page user list. Raises on ANY page failure so callers can apply
their own fail-safes; never returns a partial list (a partial member list
would make the sweep deactivate everyone missing from it).
The members connection has NO cursor/pageInfo pagination (older-style API)
and caps at 100 per page: an unpaginated query silently hides everyone
past the first page — the same failure class as Cloudron's 25-per-page
user list. Raises on ANY page failure so callers can apply their own
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:
raise RuntimeError("OC_PERSONAL_TOKEN not set; cannot fetch members")
nodes: list[dict] = []
after: str | None = None
offset = 0
async with httpx.AsyncClient() as client:
for _ in range(20): # hard cap: 20 pages × 100 = 1000 members
after_q = f', after: "{after}"' if after else ""
for _ in range(20): # hard cap: 20 pages × 100 = 2000 members
query = (
'{ collective(slug: "%s") { members(limit: 100%s) { nodes '
'{ role tier { name } account { name slug type ... on Individual { email } } } '
'pageInfo { hasNextPage endCursor } } } }'
% (OC_COLLECTIVE_SLUG, after_q)
'{ collective(slug: "%s") { members(limit: 100, offset: %d) { '
"totalCount nodes { role tier { name } account { name slug type ... on Individual { email } } } } } }"
% (OC_COLLECTIVE_SLUG, offset)
)
r = await client.post(
OC_GRAPHQL_URL,
@@ -674,12 +673,13 @@ async def fetch_all_member_nodes() -> list[dict]:
)
if r.status_code != 200:
raise RuntimeError(f"OC member fetch failed: HTTP {r.status_code}")
data = ((r.json().get("data") or {}).get("collective") or {}).get("members") or {}
nodes.extend(data.get("nodes") or [])
info = data.get("pageInfo") or {}
if not info.get("hasNextPage") or not info.get("endCursor"):
members = ((r.json().get("data") or {}).get("collective") or {}).get("members") or {}
total = members.get("totalCount") or 0
page = members.get("nodes") or []
nodes.extend(page)
offset += len(page)
if not page or offset >= total:
return nodes
after = info["endCursor"]
raise RuntimeError("OC member pagination exceeded 20 pages")