forked from baron/baron-sso
107 lines
2.7 KiB
TypeScript
107 lines
2.7 KiB
TypeScript
export type CursorPageResponse<TItem> = {
|
|
items: TItem[];
|
|
limit?: number;
|
|
offset?: number;
|
|
total?: number;
|
|
cursor?: string;
|
|
nextCursor?: string;
|
|
next_cursor?: string;
|
|
};
|
|
|
|
export type CursorFetchParams = Record<
|
|
string,
|
|
string | number | boolean | null | undefined
|
|
>;
|
|
|
|
export type CursorFetchRequest = {
|
|
baseUrl: string;
|
|
path: string;
|
|
pageSize?: number;
|
|
params?: CursorFetchParams;
|
|
headers?: Record<string, string>;
|
|
credentials?: RequestCredentials;
|
|
maxPages?: number;
|
|
};
|
|
|
|
function normalizeBaseUrl(baseUrl: string) {
|
|
const value = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
return new URL(value, globalThis.location?.origin ?? "http://localhost");
|
|
}
|
|
|
|
function buildCursorFetchUrl(
|
|
request: Required<Pick<CursorFetchRequest, "baseUrl" | "path">> &
|
|
Pick<CursorFetchRequest, "params">,
|
|
pageSize: number,
|
|
cursor: string | undefined,
|
|
) {
|
|
const path = request.path.replace(/^\/+/, "");
|
|
const url = new URL(path, normalizeBaseUrl(request.baseUrl));
|
|
|
|
for (const [key, value] of Object.entries(request.params ?? {})) {
|
|
if (value !== undefined && value !== null && value !== "") {
|
|
url.searchParams.set(key, String(value));
|
|
}
|
|
}
|
|
|
|
url.searchParams.set("limit", String(pageSize));
|
|
url.searchParams.set("offset", "0");
|
|
if (cursor) {
|
|
url.searchParams.set("cursor", cursor);
|
|
} else {
|
|
url.searchParams.delete("cursor");
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
function readNextCursor<TItem>(page: CursorPageResponse<TItem>) {
|
|
return page.nextCursor || page.next_cursor || undefined;
|
|
}
|
|
|
|
export async function fetchAllCursorPagesMainThread<TItem>({
|
|
pageSize = 100,
|
|
credentials = "same-origin",
|
|
maxPages = 1000,
|
|
...request
|
|
}: CursorFetchRequest): Promise<CursorPageResponse<TItem>> {
|
|
const items: TItem[] = [];
|
|
let cursor: string | undefined;
|
|
|
|
for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) {
|
|
const url = buildCursorFetchUrl(request, pageSize, cursor);
|
|
const response = await fetch(url, {
|
|
headers: request.headers,
|
|
credentials,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Cursor page request failed with status ${response.status}`);
|
|
}
|
|
|
|
const page = (await response.json()) as CursorPageResponse<TItem>;
|
|
items.push(...page.items);
|
|
|
|
const nextCursor = readNextCursor(page);
|
|
if (!nextCursor) {
|
|
return {
|
|
...page,
|
|
items,
|
|
limit: pageSize,
|
|
offset: 0,
|
|
total: items.length,
|
|
cursor,
|
|
nextCursor: undefined,
|
|
next_cursor: undefined,
|
|
};
|
|
}
|
|
|
|
if (nextCursor === cursor) {
|
|
throw new Error("Cursor page request returned the same next cursor");
|
|
}
|
|
|
|
cursor = nextCursor;
|
|
}
|
|
|
|
throw new Error(`Cursor page request exceeded ${maxPages} pages`);
|
|
}
|