feat: complete issue list filters, sort, and fields (task 0002)
Finish the `issue list` flag surface left minimal by the tracer slice: - Server-side filters `--label`, `--assignee`, `--author`, `--milestone`, mapped to Gitea's `labels`, `assigned_by`, `created_by`, `milestones`. - Client-side `--sort <created|updated|comments>`, always descending, over the fully paginated set (ADR 0005); `--limit` caps the sorted result. - `--fields` exposing body, closedAt, labels, milestone, updatedAt, url. - `--search` refused with a VALIDATION_ERROR redirecting to `search issues`. Exhaustive pagination lands as a shared `paginate.ts`, since ADR 0005 makes it a policy later slices reuse; `lookup.ts` drops its hand-rolled copy of the same loop. The helper carries the 20-page cap that copy encoded, matching the 1000-item ceiling Principle 8 sets.
This commit is contained in:
56
src/paginate.ts
Normal file
56
src/paginate.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
/**
|
||||
* Guard against an unbounded loop if a server ignores paging and always returns
|
||||
* a full page. 20 pages of 50 is the 1000-item cap the spec sets on exhaustive
|
||||
* pagination (Principle 8).
|
||||
*/
|
||||
const PAGE_LIMIT = 20;
|
||||
|
||||
interface PageResponse<T> {
|
||||
data?: T[];
|
||||
headers: Headers;
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[];
|
||||
/** `X-Total-Count`, absent when the header is missing or not a number. */
|
||||
total: number | undefined;
|
||||
}
|
||||
|
||||
export function readTotalCount(headers: Headers): number | undefined {
|
||||
const raw = headers.get("x-total-count");
|
||||
if (raw === null) {
|
||||
return undefined;
|
||||
}
|
||||
const total = Number(raw);
|
||||
return Number.isFinite(total) ? total : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read every page, stopping at the first short one or at the page cap. Needed by
|
||||
* the client-side policies (see ADR 0005): a command sorting or filtering
|
||||
* in-process cannot do either correctly until it holds the whole set.
|
||||
*
|
||||
* The total comes from the first page and describes the set the API returned, so
|
||||
* it stays accurate under sorting (which only reorders) but not under
|
||||
* client-side filtering, whose caller counts the filtered set itself.
|
||||
*/
|
||||
export async function fetchAllPages<T>(
|
||||
fetchPage: (page: number, limit: number) => Promise<PageResponse<T>>,
|
||||
): Promise<PaginatedResult<T>> {
|
||||
const items: T[] = [];
|
||||
let total: number | undefined;
|
||||
for (let page = 1; page <= PAGE_LIMIT; page++) {
|
||||
const response = await fetchPage(page, PAGE_SIZE);
|
||||
if (page === 1) {
|
||||
total = readTotalCount(response.headers);
|
||||
}
|
||||
const batch = response.data ?? [];
|
||||
items.push(...batch);
|
||||
if (batch.length < PAGE_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { items, total };
|
||||
}
|
||||
Reference in New Issue
Block a user