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:
1 parent
42c664c076
commit
e4da4da2c6
1 file changed
+18
-18
+18
-18
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user