Skip to content

Commit

Permalink
feat: add more advanced pagination options (#39)
Browse files Browse the repository at this point in the history
* feat: add more advanced pagination options

* feat: pagination in list api
  • Loading branch information
CompuIves authored Jan 28, 2025
1 parent 1e9e19c commit f086bb8
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 36 deletions.
13 changes: 10 additions & 3 deletions src/bin/commands/sandbox/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { CommandModule } from "yargs";
import { forkSandbox } from "./fork";
import { hibernateSandbox } from "./hibernate";
import { DEFAULT_LIMIT, listSandboxes } from "./list";
import { listSandboxes } from "./list";
import { shutdownSandbox } from "./shutdown";

const DEFAULT_LIMIT = 100;

export const sandboxCommand: CommandModule = {
command: "sandbox",
describe: "Manage sandboxes",
Expand Down Expand Up @@ -68,13 +70,18 @@ export const sandboxCommand: CommandModule = {
{
tags: argv.tags?.split(","),
status: argv.status as "running" | undefined,
page: argv.page as number | undefined,
pageSize: argv["page-size"] as number | undefined,
orderBy: argv["order-by"] as
| "inserted_at"
| "updated_at"
| undefined,
direction: argv.direction as "asc" | "desc" | undefined,
pagination:
argv.page || argv["page-size"]
? {
page: argv.page,
pageSize: argv["page-size"],
}
: undefined,
},
argv["headers"] as boolean,
argv.limit as number | undefined
Expand Down
73 changes: 53 additions & 20 deletions src/bin/commands/sandbox/list.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import ora from "ora";
import Table from "cli-table3";
import { CodeSandbox } from "../../../";
import type { SandboxListOpts, SandboxInfo } from "../../../sandbox-client";

export const DEFAULT_LIMIT = 100;
import type {
SandboxListOpts,
SandboxInfo,
PaginationOpts,
} from "../../../sandbox-client";

type OutputFormat = {
field: string;
Expand Down Expand Up @@ -41,41 +43,59 @@ function formatAge(date: Date): string {

export async function listSandboxes(
outputFields?: string,
listOpts: SandboxListOpts = {},
listOpts: SandboxListOpts & { pagination?: PaginationOpts } = {},
showHeaders = true,
limit = DEFAULT_LIMIT
limit?: number
) {
const sdk = new CodeSandbox();
const spinner = ora("Fetching sandboxes...").start();

try {
let allSandboxes: SandboxInfo[] = [];
let currentPage = listOpts.page || 1;
const pageSize = listOpts.pageSize || 20;
let totalCount = 0;
let currentPage = 1;
const pageSize = 50; // API's maximum page size

// Keep fetching until we hit the limit or run out of sandboxes
while (true) {
const { sandboxes, pagination } = await sdk.sandbox.list({
const {
sandboxes,
totalCount: total,
pagination,
} = await sdk.sandbox.list({
...listOpts,
page: currentPage,
pageSize,
limit: undefined, // Force pagination so we can show progress
pagination: {
page: currentPage,
pageSize,
},
});

allSandboxes = [...allSandboxes, ...sandboxes];

// Stop if we've hit the limit
if (allSandboxes.length >= limit) {
allSandboxes = allSandboxes.slice(0, limit);
if (sandboxes.length === 0) {
break;
}

// Stop if there are no more pages
if (!pagination.nextPage) {
totalCount = total;
const newSandboxes = sandboxes.filter(
(sandbox) =>
!allSandboxes.some((existing) => existing.id === sandbox.id)
);
allSandboxes = [...allSandboxes, ...newSandboxes];

spinner.text = `Fetching sandboxes... (${allSandboxes.length}${
limit ? `/${Math.min(limit, totalCount)}` : `/${totalCount}`
})`;

// Stop if we've reached the total count
if (allSandboxes.length >= totalCount) {
break;
}

currentPage = pagination.nextPage;
spinner.text = `Fetching sandboxes... (${allSandboxes.length}/${limit})`;
currentPage++;
}

// Apply limit after fetching all sandboxes
if (limit) {
allSandboxes = allSandboxes.slice(0, limit);
}

spinner.stop();
Expand Down Expand Up @@ -103,6 +123,12 @@ export async function listSandboxes(
// eslint-disable-next-line no-console
console.log(values.join("\t"));
});

// eslint-disable-next-line no-console
console.log(
`\nShowing ${allSandboxes.length} of ${totalCount} sandboxes`
);

return;
}

Expand Down Expand Up @@ -149,6 +175,13 @@ export async function listSandboxes(

// eslint-disable-next-line no-console
console.log(table.toString());

if (limit && totalCount > allSandboxes.length) {
// eslint-disable-next-line no-console
console.log(
`\nShowing ${allSandboxes.length} of ${totalCount} sandboxes`
);
}
} catch (error) {
spinner.fail("Failed to fetch sandboxes");
throw error;
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
VMTier,
SandboxListOpts,
SandboxInfo,
PaginationOpts,
} from "./sandbox-client";

export {
Expand All @@ -17,6 +18,7 @@ export {
VMTier,
SandboxListOpts,
SandboxInfo,
PaginationOpts,
};
export * from "./sandbox";

Expand Down
79 changes: 66 additions & 13 deletions src/sandbox-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,27 @@ export type SandboxInfo = {

export type SandboxListOpts = {
tags?: string[];
page?: number;
pageSize?: number;
orderBy?: "inserted_at" | "updated_at";
direction?: "asc" | "desc";
status?: "running";
};

export interface SandboxListResponse {
sandboxes: SandboxInfo[];
hasMore: boolean;
totalCount: number;
pagination: {
currentPage: number;
nextPage: number | null;
pageSize: number;
};
}

export type PaginationOpts = {
page?: number;
pageSize?: number;
};

export const DEFAULT_SUBSCRIPTIONS = {
client: {
status: true,
Expand Down Expand Up @@ -376,19 +390,47 @@ export class SandboxClient {

/**
* List sandboxes from the current workspace with optional filters.
* By default, returns up to 100 sandboxes.
*
* This method supports two modes of operation:
* 1. Simple limit-based fetching (default):
* ```ts
* // Get up to 100 sandboxes (default)
* const { sandboxes, totalCount } = await client.list();
*
* // Get up to 200 sandboxes
* const { sandboxes, totalCount } = await client.list({ limit: 200 });
* ```
*
* 2. Manual pagination:
* ```ts
* // Get first page
* const { sandboxes, pagination } = await client.list({
* pagination: { page: 1, pageSize: 50 }
* });
* // pagination = { currentPage: 1, nextPage: 2, pageSize: 50 }
*
* // Get next page if available
* if (pagination.nextPage) {
* const { sandboxes, pagination: nextPagination } = await client.list({
* pagination: { page: pagination.nextPage, pageSize: 50 }
* });
* }
* ```
*/
async list(
opts: Omit<SandboxListOpts, "page" | "pageSize"> & { limit?: number } = {}
): Promise<{
sandboxes: SandboxInfo[];
}> {
opts: SandboxListOpts & {
limit?: number;
pagination?: PaginationOpts;
} = {}
): Promise<SandboxListResponse> {
const limit = opts.limit ?? 100;
const pageSize = 50; // API's maximum page size
let allSandboxes: SandboxInfo[] = [];
let currentPage = 1;
let currentPage = opts.pagination?.page ?? 1;
let pageSize = opts.pagination?.pageSize ?? 50;
let totalCount = 0;
let nextPage: number | null = null;

while (allSandboxes.length < limit) {
while (true) {
const response = await sandboxList({
client: this.apiClient,
query: {
Expand All @@ -402,6 +444,8 @@ export class SandboxClient {
});

const info = handleResponse(response, "Failed to list sandboxes");
totalCount = info.pagination.total_records;
nextPage = info.pagination.next_page;

const sandboxes = info.sandboxes.map((sandbox) => ({
id: sandbox.id,
Expand All @@ -416,15 +460,24 @@ export class SandboxClient {
allSandboxes = [...allSandboxes, ...sandboxes];

// Stop if we've hit the limit or there are no more pages
if (!info.pagination.next_page || allSandboxes.length >= limit) {
if (!nextPage || allSandboxes.length >= limit) {
allSandboxes = allSandboxes.slice(0, limit);
break;
}

currentPage = info.pagination.next_page;
currentPage = nextPage;
}

return { sandboxes: allSandboxes };
return {
sandboxes: allSandboxes,
hasMore: totalCount > allSandboxes.length,
totalCount,
pagination: {
currentPage,
nextPage: allSandboxes.length >= limit ? nextPage : null,
pageSize,
},
};
}

/**
Expand Down

0 comments on commit f086bb8

Please sign in to comment.