Skip to content

Commit 5a6a8d7

Browse files
committed
Add pagination info to table
1 parent 3407e5d commit 5a6a8d7

File tree

2 files changed

+110
-51
lines changed

2 files changed

+110
-51
lines changed

src/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,12 @@ async function main(): Promise<void> {
117117

118118
const result = await command.execute(client, params);
119119

120-
const format = parsed.globalFlags.output ?? getDefaultFormat();
121-
console.log(formatOutput(result, format, parsed.globalFlags.columns)); // eslint-disable-line no-console
120+
const format =
121+
parsed.globalFlags.output ??
122+
(parsed.globalFlags.columns ? "table" : getDefaultFormat());
123+
console.log(
124+
formatOutput(result, format, parsed.globalFlags.columns, params)
125+
); // eslint-disable-line no-console
122126
} finally {
123127
client.destroy();
124128
}

src/output.ts

Lines changed: 104 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,39 @@ 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 : 1;
187+
const pageSize =
188+
typeof params?.pageSize === "number" ? params.pageSize : rowCount;
189+
const start = (page - 1) * pageSize + 1;
190+
const end = start + rowCount - 1;
191+
192+
let summary: string;
193+
if (rowCount === 0) {
194+
summary =
195+
total !== undefined ? `No results (${total} total)` : "No results";
196+
} else if (total !== undefined) {
197+
summary = `Showing ${start}-${end} of ${total}`;
198+
} else {
199+
summary = `Showing ${start}-${end}`;
200+
}
201+
if (hasMore) summary += ` (next: --page ${page + 1} --pageSize ${pageSize})`;
202+
203+
return "\n" + theme.muted(summary);
204+
}
205+
151206
function formatCellValue(value: unknown): string {
152207
if (value === null || value === undefined) return theme.muted("null");
153208
if (typeof value === "object") {

0 commit comments

Comments
 (0)