import cloudflareApiV4, { type CloudflareApiV4Result } from './api-v4.ts' import type { CloudflareAuthOptions } from './auth.ts' export type CloudflareApiV4ListInput = CloudflareAuthOptions & { /** List endpoint path starting with `/client/v4/`. */ path: string /** Extra query params. `page`, `per_page`, and `cursor` are managed here. */ query?: Record /** * Page size. Omit to use the endpoint's default (safest: Pages project * lists reject `per_page` above 10 with error 8000024). */ perPage?: number /** Stop after this many requests (default 50). */ maxPages?: number /** Stop after collecting this many items (default unlimited). */ maxItems?: number /** * Property holding the array when `result` is an object, e.g. `buckets` * for R2. Auto-detected when `result` has exactly one array property. */ itemsKey?: string } export type CloudflareApiV4ListResult = { ok: boolean items: T[] /** Requests made. */ pages: number /** `result_info.total_count` when the endpoint reports it. */ totalCount: number | null /** True when maxPages / maxItems stopped the walk before the end. */ truncated: boolean httpStatus: number | null errors: unknown[] } type ResultInfo = { page?: number per_page?: number total_pages?: number total_count?: number count?: number cursor?: string cursors?: { after?: string } } const MANAGED_QUERY_KEYS = ['page', 'per_page', 'cursor'] /** Pull the item array out of a list response (`result` array or one array property). */ export function extractListItems(result: unknown, itemsKey?: string): unknown[] { if (Array.isArray(result)) return result if (!result || typeof result !== 'object') return [] const record = result as Record if (itemsKey) { const value = record[itemsKey] if (!Array.isArray(value)) { throw new Error(`result.${itemsKey} is not an array.`) } return value } const arrays = Object.values(record).filter(Array.isArray) return arrays.length === 1 ? (arrays[0] as unknown[]) : [] } /** * Decide the next request from `result_info`: a cursor, the next page number, * or `null` when the list is done. */ export function nextPageQuery( info: ResultInfo | null | undefined, state: { page: number; received: number; perPage?: number }, ): { cursor: string } | { page: number } | null { if (!info || typeof info !== 'object') return null const cursor = info.cursor || info.cursors?.after if (typeof cursor === 'string' && cursor) { return state.received === 0 ? null : { cursor } } if (typeof info.total_pages === 'number') { const current = typeof info.page === 'number' ? info.page : state.page return current < info.total_pages ? { page: current + 1 } : null } const size = state.perPage ?? info.per_page if (typeof info.page === 'number' && typeof size === 'number' && state.received >= size) { return { page: info.page + 1 } } return null } /** * Fetch every page of a Cloudflare API v4 list endpoint into one array. * Handles page-number (`result_info.total_pages`) and cursor * (`result_info.cursor`) pagination. Read-only (`GET`). * * @param params.path - List path under `/client/v4/`. * @param params.perPage - Page size; omit for the endpoint default (Pages max is 10). * @param params.maxPages - Request cap (default 50). * @param params.account - Token alias (`default`, `kody`, `pages`). * @returns `{ ok, items, pages, totalCount, truncated, errors }`. * @example * import listAll from 'kody:@kentcdodds/cloudflare/api-v4-list' * const { items } = await listAll({ * account: 'pages', * path: '/client/v4/accounts/ACCOUNT_ID/pages/projects', * }) * // => items: [{ name: 'my-site', subdomain: 'my-site.pages.dev', ... }, ...] */ export default async function cloudflareApiV4List( params: CloudflareApiV4ListInput, ): Promise> { if (!params || typeof params.path !== 'string') { throw new Error('path is required (starting with /client/v4/).') } const query = { ...(params.query ?? {}) } for (const key of MANAGED_QUERY_KEYS) { if (key in query) throw new Error(`Do not pass "${key}" in query; the list helper manages it.`) } if (params.perPage !== undefined && (!Number.isInteger(params.perPage) || params.perPage < 1)) { throw new Error('perPage must be a positive integer.') } const maxPages = params.maxPages ?? 50 const maxItems = params.maxItems ?? Number.POSITIVE_INFINITY const items: T[] = [] let pages = 0 let totalCount: number | null = null let httpStatus: number | null = null let next: { cursor: string } | { page: number } | null = { page: 1 } let truncated = false while (next) { if (pages >= maxPages || items.length >= maxItems) { truncated = true break } const pageQuery: Record = { ...query } if ('cursor' in next) pageQuery.cursor = next.cursor else if (next.page > 1) pageQuery.page = next.page if (params.perPage !== undefined) pageQuery.per_page = params.perPage const response: CloudflareApiV4Result = await cloudflareApiV4({ account: params.account, apiTokenSecret: params.apiTokenSecret, method: 'GET', path: params.path, query: pageQuery, }) pages += 1 httpStatus = response.httpStatus if (!response.success) { return { ok: false, items, pages, totalCount, truncated: false, httpStatus, errors: response.errors } } const batch = extractListItems(response.result, params.itemsKey) as T[] items.push(...batch) const info = (response.resultInfo ?? null) as ResultInfo | null if (info && typeof info.total_count === 'number') totalCount = info.total_count next = nextPageQuery(info, { page: 'page' in next ? next.page : pages, received: batch.length, perPage: params.perPage, }) } if (items.length > maxItems) items.length = maxItems return { ok: true, items, pages, totalCount, truncated, httpStatus, errors: [] } }