Skip to content

Commit 02e8894

Browse files
committed
Add pagination info to table
1 parent 3407e5d commit 02e8894

2 files changed

Lines changed: 133 additions & 51 deletions

File tree

src/index.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,20 @@ async function main(): Promise<void> {
9595
command.cliTransforms
9696
);
9797

98+
const CLI_PAGINATION_DEFAULTS: Record<string, number> = {
99+
page: 1,
100+
pageSize: 10,
101+
};
102+
const schemaKeys =
103+
command.schema instanceof z.ZodObject
104+
? new Set(Object.keys(command.schema.shape))
105+
: new Set<string>();
106+
for (const [key, defaultVal] of Object.entries(CLI_PAGINATION_DEFAULTS)) {
107+
if (schemaKeys.has(key)) {
108+
params[key] ??= defaultVal;
109+
}
110+
}
111+
98112
if (
99113
command.destructive &&
100114
!parsed.globalFlags.force &&
@@ -117,8 +131,12 @@ async function main(): Promise<void> {
117131

118132
const result = await command.execute(client, params);
119133

120-
const format = parsed.globalFlags.output ?? getDefaultFormat();
121-
console.log(formatOutput(result, format, parsed.globalFlags.columns)); // eslint-disable-line no-console
134+
const format =
135+
parsed.globalFlags.output ??
136+
(parsed.globalFlags.columns ? "table" : getDefaultFormat());
137+
console.log(
138+
formatOutput(result, format, parsed.globalFlags.columns, params)
139+
); // eslint-disable-line no-console
122140
} finally {
123141
client.destroy();
124142
}

src/output.ts

Lines changed: 113 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ export function getDefaultFormat(): OutputFormat {
1212
export function formatOutput(
1313
data: unknown,
1414
format: OutputFormat,
15-
columns?: string[]
15+
columns?: string[],
16+
params?: Record<string, unknown>
1617
): string {
1718
switch (format) {
1819
case "json":
1920
return JSON.stringify(data, null, 2);
2021
case "pretty":
2122
return colorizeJson(data);
2223
case "table":
23-
return formatTable(data, columns);
24+
return formatTable(data, columns, params);
2425
default: {
2526
const _exhaustive: never = format;
2627
throw new Error(`Unknown format: ${_exhaustive}`);
@@ -58,78 +59,99 @@ function colorizeJson(data: unknown, indent = 0): string {
5859
return String(data);
5960
}
6061

61-
function findTableData(data: unknown): unknown[] | null {
62-
if (Array.isArray(data)) return data;
62+
interface TableData {
63+
rows: unknown[];
64+
metadata: Record<string, unknown>;
65+
}
66+
67+
function findTableData(data: unknown): TableData | null {
68+
if (Array.isArray(data)) return { rows: data, metadata: {} };
6369
if (data && typeof data === "object") {
6470
const entries = Object.entries(data as Record<string, unknown>);
65-
for (const [, value] of entries) {
66-
if (Array.isArray(value) && value.length > 0) {
67-
return value;
71+
for (const [arrayKey, value] of entries) {
72+
if (Array.isArray(value)) {
73+
const metadata: Record<string, unknown> = {};
74+
for (const [key, val] of entries) {
75+
if (key !== arrayKey) metadata[key] = val;
76+
}
77+
return { rows: value, metadata };
6878
}
6979
}
7080
}
7181
return null;
7282
}
7383

74-
function formatTable(data: unknown, columns?: string[]): string {
84+
function collectKeys(rows: unknown[]): string[] {
85+
const keys = new Set<string>();
86+
for (const item of rows) {
87+
if (item && typeof item === "object") {
88+
for (const k of Object.keys(item as Record<string, unknown>)) {
89+
keys.add(k);
90+
}
91+
}
92+
}
93+
return [...keys];
94+
}
95+
96+
function resolveColumns(
97+
requested: string[] | undefined,
98+
available: string[]
99+
): string[] {
100+
if (!requested || requested.length === 0) return available;
101+
102+
const validSet = new Set(available);
103+
const invalid = requested.filter((c) => !validSet.has(c));
104+
if (invalid.length > 0) {
105+
console.error(
106+
// eslint-disable-line no-console
107+
`Warning: unknown column(s): ${invalid.join(", ")}. Available: ${available.join(", ")}`
108+
);
109+
}
110+
const valid = requested.filter((c) => validSet.has(c));
111+
return valid.length > 0 ? valid : available;
112+
}
113+
114+
function formatTable(
115+
data: unknown,
116+
columns?: string[],
117+
params?: Record<string, unknown>
118+
): string {
75119
if (data === null || data === undefined) {
76120
return theme.muted("(empty)");
77121
}
78122

79-
const arrayData = findTableData(data);
80-
81-
if (arrayData && arrayData.length > 0) {
82-
const firstItem = arrayData[0];
83-
if (firstItem && typeof firstItem === "object") {
84-
let selectedColumns: string[];
85-
if (columns && columns.length > 0) {
86-
const allKeys = new Set<string>();
87-
for (const item of arrayData) {
88-
if (item && typeof item === "object") {
89-
Object.keys(item as Record<string, unknown>).forEach((k) =>
90-
allKeys.add(k)
91-
);
92-
}
93-
}
94-
const invalid = columns.filter((c) => !allKeys.has(c));
95-
if (invalid.length > 0) {
96-
console.error(
97-
// eslint-disable-line no-console
98-
`Warning: unknown column(s): ${invalid.join(", ")}. Available: ${[...allKeys].join(", ")}`
99-
);
100-
}
101-
selectedColumns = columns.filter((c) => allKeys.has(c));
102-
if (selectedColumns.length === 0) {
103-
selectedColumns = [...allKeys];
104-
}
105-
} else {
106-
const allKeys = new Set<string>();
107-
for (const item of arrayData) {
108-
if (item && typeof item === "object") {
109-
Object.keys(item as Record<string, unknown>).forEach((k) =>
110-
allKeys.add(k)
111-
);
112-
}
113-
}
114-
selectedColumns = [...allKeys];
115-
}
123+
const tableData = findTableData(data);
124+
125+
if (tableData) {
126+
const { rows, metadata } = tableData;
127+
const meta = formatMetadata(metadata, rows.length, params);
128+
129+
if (rows.length === 0) {
130+
return meta ? meta.trimStart() : theme.muted("No results");
131+
}
132+
133+
const isObjectRows = rows[0] && typeof rows[0] === "object";
134+
if (isObjectRows) {
135+
const selectedColumns = resolveColumns(columns, collectKeys(rows));
116136
const table = new Table({
117137
head: selectedColumns.map((c) => theme.bold(c)),
118138
wordWrap: true,
119139
});
120-
for (const item of arrayData) {
140+
for (const item of rows) {
121141
const obj = (item ?? {}) as Record<string, unknown>;
122142
table.push(selectedColumns.map((col) => formatCellValue(obj[col])));
123143
}
124-
return table.toString();
144+
return table.toString() + meta;
125145
}
146+
126147
const table = new Table({ head: [theme.bold("Value")] });
127-
for (const item of arrayData) {
148+
for (const item of rows) {
128149
table.push([formatCellValue(item)]);
129150
}
130-
return table.toString();
151+
return table.toString() + meta;
131152
}
132153

154+
// Single object: key-value table
133155
if (data && typeof data === "object" && !Array.isArray(data)) {
134156
const entries = Object.entries(data as Record<string, unknown>);
135157
const filteredEntries =
@@ -148,6 +170,48 @@ function formatTable(data: unknown, columns?: string[]): string {
148170
return String(data);
149171
}
150172

173+
function formatMetadata(
174+
metadata: Record<string, unknown>,
175+
rowCount: number,
176+
params?: Record<string, unknown>
177+
): string {
178+
const totalKey = Object.keys(metadata).find(
179+
(k) => k.startsWith("total") && typeof metadata[k] === "number"
180+
);
181+
const total = totalKey ? (metadata[totalKey] as number) : undefined;
182+
const hasMore = typeof metadata.nextPageUrl === "string";
183+
184+
if (total === undefined && !hasMore) return "";
185+
186+
const page = typeof params?.page === "number" ? params.page : undefined;
187+
const pageSize =
188+
typeof params?.pageSize === "number" ? params.pageSize : undefined;
189+
190+
let summary: string;
191+
if (rowCount === 0) {
192+
summary =
193+
total !== undefined ? `No results (${total} total)` : "No results";
194+
} else if (page !== undefined && pageSize !== undefined) {
195+
const start = (page - 1) * pageSize + 1;
196+
const end = start + rowCount - 1;
197+
summary =
198+
total !== undefined
199+
? `Showing ${start}-${end} of ${total}`
200+
: `Showing ${start}-${end}`;
201+
} else if (total !== undefined) {
202+
summary = `Showing ${rowCount} of ${total}`;
203+
} else {
204+
summary = `Showing ${rowCount} results`;
205+
}
206+
if (hasMore && page !== undefined && pageSize !== undefined) {
207+
summary += ` (next: --page ${page + 1} --pageSize ${pageSize})`;
208+
} else if (hasMore) {
209+
summary += " (more results available)";
210+
}
211+
212+
return "\n" + theme.muted(summary);
213+
}
214+
151215
function formatCellValue(value: unknown): string {
152216
if (value === null || value === undefined) return theme.muted("null");
153217
if (typeof value === "object") {

0 commit comments

Comments
 (0)