diff --git a/.changeset/orange-lamps-happen.md b/.changeset/orange-lamps-happen.md new file mode 100644 index 000000000000..07b76e56c993 --- /dev/null +++ b/.changeset/orange-lamps-happen.md @@ -0,0 +1,9 @@ +--- +"wrangler": patch +--- + +This PR adds a fetch handler that uses `page`, assuming `result_info` provided by the endpoint contains `page`, `per_page`, and `total` + +This is needed as the existing `fetchListResult` handler for fetching potentially paginated results doesn't work for endpoints that don't implement `cursor`. + +Fixes #4349 diff --git a/packages/wrangler/src/__tests__/cfetch-utils.test.ts b/packages/wrangler/src/__tests__/cfetch-utils.test.ts new file mode 100644 index 000000000000..ffa2b6c277a2 --- /dev/null +++ b/packages/wrangler/src/__tests__/cfetch-utils.test.ts @@ -0,0 +1,38 @@ +import { hasMorePages } from "../cfetch"; + +/** +hasMorePages is a function that returns a boolean based on the result_info object returned from the cloudflare v4 API - if the current page is less than the total number of pages, it returns true, otherwise false. +*/ + +describe("hasMorePages", () => { + it("should handle result_info not having enough results to paginate", () => { + expect( + hasMorePages({ + page: 1, + per_page: 10, + count: 5, + total_count: 5, + }) + ).toBe(false); + }); + it("should return true if the current page is less than the total number of pages", () => { + expect( + hasMorePages({ + page: 1, + per_page: 10, + count: 10, + total_count: 100, + }) + ).toBe(true); + }); + it("should return false if we are on the last page of results", () => { + expect( + hasMorePages({ + page: 10, + per_page: 10, + count: 10, + total_count: 100, + }) + ).toBe(false); + }); +}); diff --git a/packages/wrangler/src/cfetch/index.ts b/packages/wrangler/src/cfetch/index.ts index 34a169c910fe..c148f212b04c 100644 --- a/packages/wrangler/src/cfetch/index.ts +++ b/packages/wrangler/src/cfetch/index.ts @@ -100,6 +100,66 @@ export async function fetchListResult( return results; } +/** + * Make a fetch request for a list of values using pages, + * extracting the `result` from the JSON response, + * and repeating the request if the results are paginated. + * + * This is similar to fetchListResult, but it uses the `page` query parameter instead of `cursor`. + */ +export async function fetchPagedListResult( + resource: string, + init: RequestInit = {}, + queryParams?: URLSearchParams +): Promise { + const results: ResponseType[] = []; + let getMoreResults = true; + let page = 1; + while (getMoreResults) { + queryParams = new URLSearchParams(queryParams); + queryParams.set("page", String(page)); + + const json = await fetchInternal>( + resource, + init, + queryParams + ); + if (json.success) { + results.push(...json.result); + if (hasMorePages(json.result_info)) { + page = page + 1; + } else { + getMoreResults = false; + } + } else { + throwFetchError(resource, json); + } + } + return results; +} + +interface PageResultInfo { + page: number; + per_page: number; + count: number; + total_count: number; +} + +export function hasMorePages( + result_info: unknown +): result_info is PageResultInfo { + const page = (result_info as PageResultInfo | undefined)?.page; + const per_page = (result_info as PageResultInfo | undefined)?.per_page; + const total = (result_info as PageResultInfo | undefined)?.total_count; + + return ( + page !== undefined && + per_page !== undefined && + total !== undefined && + page * per_page < total + ); +} + function throwFetchError( resource: string, response: FetchResult diff --git a/packages/wrangler/src/user/choose-account.tsx b/packages/wrangler/src/user/choose-account.tsx index eaea33073140..76cb7f336459 100644 --- a/packages/wrangler/src/user/choose-account.tsx +++ b/packages/wrangler/src/user/choose-account.tsx @@ -1,4 +1,4 @@ -import { fetchListResult } from "../cfetch"; +import { fetchPagedListResult } from "../cfetch"; import { getCloudflareAccountIdFromEnv } from "./auth-variables"; export type ChooseAccountItem = { @@ -15,7 +15,7 @@ export async function getAccountChoices(): Promise { return [{ id: accountIdFromEnv, name: "" }]; } else { try { - const response = await fetchListResult<{ + const response = await fetchPagedListResult<{ account: ChooseAccountItem; }>(`/memberships`); const accounts = response.map((r) => r.account); diff --git a/packages/wrangler/src/whoami.ts b/packages/wrangler/src/whoami.ts index c3b166ba8554..80b87676ce13 100644 --- a/packages/wrangler/src/whoami.ts +++ b/packages/wrangler/src/whoami.ts @@ -1,5 +1,5 @@ import chalk from "chalk"; -import { fetchListResult, fetchResult } from "./cfetch"; +import { fetchPagedListResult, fetchResult } from "./cfetch"; import { logger } from "./logger"; import { getAPIToken, getAuthFromEnv, getScopes } from "./user"; @@ -91,7 +91,7 @@ async function getEmail(): Promise { type AccountInfo = { name: string; id: string }; async function getAccounts(): Promise { - return await fetchListResult("/accounts"); + return await fetchPagedListResult("/accounts"); } async function getTokenPermissions(): Promise {