Skip to content

Commit bbf9417

Browse files
committed
fix(access-tools): drop dead action filter; page listGrants fully (CL-7465)
1 parent 2594243 commit bbf9417

2 files changed

Lines changed: 55 additions & 25 deletions

File tree

‎packages/access-tools/src/client.test.ts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,30 @@ test("listGrants passes filters through and reads the native page envelope", asy
9797
});
9898
});
9999

100+
test("listGrants follows nextCursor so a full page plus one all return", async () => {
101+
const all = Array.from({ length: 51 }, (_, i) =>
102+
nativeGrant(`grant_${i + 1}`),
103+
);
104+
const seen: string[] = [];
105+
const fetchImpl = (async (url: string | URL | Request) => {
106+
const raw = String(url);
107+
seen.push(raw);
108+
if (raw.includes("cursor=page_2")) {
109+
return Response.json({ data: all.slice(50), nextCursor: null });
110+
}
111+
return Response.json({ data: all.slice(0, 50), nextCursor: "page_2" });
112+
}) as unknown as typeof fetch;
113+
114+
const grants = await listGrants(testConfig(fetchImpl), {
115+
principalId: "prin_2",
116+
});
117+
expect(grants).toHaveLength(51);
118+
expect(grants.map((g) => g.id)).toEqual(all.map((g) => g.id));
119+
expect(seen).toHaveLength(2);
120+
expect(seen[0]).toContain("principalId=prin_2");
121+
expect(seen[1]).toContain("cursor=page_2");
122+
});
123+
100124
test("grantAccess posts one native single-action body per action and parses single GrantResponse objects", async () => {
101125
const posted: unknown[] = [];
102126
const fetchImpl = (async (
@@ -191,7 +215,7 @@ test("revokeAccess treats a native 204 with no body as success", async () => {
191215
await revokeAccess(testConfig(fetchImpl), "grant_1");
192216
});
193217

194-
test("a grant body without principalId is a shape error, never a null-linked grant", async () => {
218+
test("a grant body missing principalId is a shape error, never a null-linked grant", async () => {
195219
const { principalId: _link, ...linkless } = nativeGrant("grant_1");
196220
void _link;
197221
const fetchImpl = (async () =>

‎packages/access-tools/src/client.ts‎

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -154,37 +154,43 @@ export async function listPrincipals(
154154
export interface ListGrantsFilter {
155155
readonly principalId?: string;
156156
readonly resource?: string;
157-
readonly action?: string;
158157
}
159158

160-
/** Lists grants in the run's tenant, passing the given filters through as
161-
* the native `GET /grants` query params (`principalId`, `resource`,
162-
* `action`). */
159+
/** Lists every grant in the run's tenant matching the given filters,
160+
* following the native `nextCursor` pages to the end — the mirror honors
161+
* the same `principalId`/`resource` query params as the native
162+
* `GET /grants`. */
163163
export async function listGrants(
164164
config: AccessToolClientConfig,
165165
filter?: ListGrantsFilter,
166166
): Promise<ListedGrant[]> {
167167
const fetchImpl = config.fetchImpl ?? ((...args) => fetch(...args));
168-
const params = new URLSearchParams();
169-
if (filter?.principalId !== undefined)
170-
params.set("principalId", filter.principalId);
171-
if (filter?.resource !== undefined) params.set("resource", filter.resource);
172-
if (filter?.action !== undefined) params.set("action", filter.action);
173-
const query = params.size === 0 ? "" : `?${params.toString()}`;
174-
const response = await fetchImpl(`${config.hubAccessUrl}/grants${query}`, {
175-
headers: { ...authHeaders(config) },
176-
});
177-
if (!response.ok) {
178-
await throwForStatus("Listing grants", response);
179-
}
180-
const body: unknown = await response.json();
181-
const parsed = ListedGrantsResponse(body);
182-
if (parsed instanceof type.errors) {
183-
throw new Error(
184-
`List-grants response did not match the expected shape: ${parsed.summary}`,
185-
);
186-
}
187-
return parsed.data;
168+
const grants: ListedGrant[] = [];
169+
let cursor: string | null | undefined;
170+
do {
171+
const params = new URLSearchParams();
172+
if (filter?.principalId !== undefined)
173+
params.set("principalId", filter.principalId);
174+
if (filter?.resource !== undefined) params.set("resource", filter.resource);
175+
if (cursor !== undefined && cursor !== null) params.set("cursor", cursor);
176+
const query = params.size === 0 ? "" : `?${params.toString()}`;
177+
const response = await fetchImpl(`${config.hubAccessUrl}/grants${query}`, {
178+
headers: { ...authHeaders(config) },
179+
});
180+
if (!response.ok) {
181+
await throwForStatus("Listing grants", response);
182+
}
183+
const body: unknown = await response.json();
184+
const parsed = ListedGrantsResponse(body);
185+
if (parsed instanceof type.errors) {
186+
throw new Error(
187+
`List-grants response did not match the expected shape: ${parsed.summary}`,
188+
);
189+
}
190+
grants.push(...parsed.data);
191+
cursor = parsed.nextCursor ?? null;
192+
} while (cursor !== null);
193+
return grants;
188194
}
189195

190196
/** Grants access by posting one native single-action `POST /grants` body

0 commit comments

Comments
 (0)