Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 97 additions & 98 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"@aws-sdk/credential-providers": "^3.1024.0",
"@smithy/shared-ini-file-loader": "^4.4.7",
"@tigrisdata/iam": "^1.4.1",
"@tigrisdata/storage": "^2.15.6",
"@tigrisdata/storage": "^2.16.2",
"commander": "^14.0.3",
"enquirer": "^2.4.1",
"jose": "^6.2.2",
Expand Down
36 changes: 30 additions & 6 deletions src/lib/access-keys/list.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { getIAMConfig } from '@auth/iam.js';
import { listAccessKeys } from '@tigrisdata/iam';
import { failWithError } from '@utils/exit.js';
import { formatOutput } from '@utils/format.js';
import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js';
import { getFormat } from '@utils/options.js';
import { formatOutput, formatPaginatedOutput } from '@utils/format.js';
import {
msg,
printEmpty,
printPaginationHint,
printStart,
printSuccess,
} from '@utils/messages.js';
import { getFormat, getPaginationOptions } from '@utils/options.js';

const context = msg('access-keys', 'list');

export default async function list(options: Record<string, unknown>) {
printStart(context);

const format = getFormat(options);
const { limit, pageToken, isPaginated } = getPaginationOptions(options);

const config = await getIAMConfig(context);

const { data, error } = await listAccessKeys({ config });
const { data, error } = await listAccessKeys({
...(limit !== undefined ? { limit } : {}),
...(pageToken ? { paginationToken: pageToken } : {}),
config,
});

if (error) {
failWithError(context, error);
Expand All @@ -32,13 +43,26 @@ export default async function list(options: Record<string, unknown>) {
created: key.createdAt,
}));

const output = formatOutput(keys, format!, 'keys', 'key', [
const columns = [
{ key: 'name', header: 'Name' },
{ key: 'id', header: 'ID' },
{ key: 'status', header: 'Status' },
{ key: 'created', header: 'Created' },
]);
];

const nextToken = data.paginationToken || undefined;

const output = isPaginated
? formatPaginatedOutput(keys, format!, 'keys', 'key', columns, {
paginationToken: nextToken,
})
: formatOutput(keys, format!, 'keys', 'key', columns);

console.log(output);

if (isPaginated && format !== 'json' && format !== 'xml') {
printPaginationHint(nextToken);
}

printSuccess(context, { count: keys.length });
}
46 changes: 40 additions & 6 deletions src/lib/buckets/list.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { getStorageConfig } from '@auth/provider.js';
import { getBucketInfo, listBuckets } from '@tigrisdata/storage';
import { failWithError } from '@utils/exit.js';
import { formatOutput } from '@utils/format.js';
import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js';
import { getFormat, getOption } from '@utils/options.js';
import { formatOutput, formatPaginatedOutput } from '@utils/format.js';
import {
msg,
printEmpty,
printPaginationHint,
printStart,
printSuccess,
} from '@utils/messages.js';
import { getFormat, getOption, getPaginationOptions } from '@utils/options.js';

const context = msg('buckets', 'list');

Expand All @@ -12,9 +18,24 @@ export default async function list(options: Record<string, unknown>) {

const format = getFormat(options);
const forksOf = getOption<string>(options, ['forks-of', 'forksOf']);
const { limit, pageToken, isPaginated } = getPaginationOptions(options);
const config = await getStorageConfig();

const { data, error } = await listBuckets({ config });
if (forksOf && isPaginated) {
console.warn(
'⚠ Pagination flags are ignored when --forks-of is used (all buckets are fetched for filtering)'
);
}

const { data, error } = await listBuckets({
...(forksOf || !isPaginated
? {}
: {
...(limit !== undefined ? { limit } : {}),
...(pageToken ? { paginationToken: pageToken } : {}),
}),
config,
});

if (error) {
failWithError(context, error);
Expand Down Expand Up @@ -71,11 +92,24 @@ export default async function list(options: Record<string, unknown>) {
created: bucket.creationDate,
}));

const output = formatOutput(buckets, format!, 'buckets', 'bucket', [
const columns = [
{ key: 'name', header: 'Name' },
{ key: 'created', header: 'Created' },
]);
];

const nextToken = data.paginationToken || undefined;

const output = isPaginated
? formatPaginatedOutput(buckets, format!, 'buckets', 'bucket', columns, {
paginationToken: nextToken,
})
: formatOutput(buckets, format!, 'buckets', 'bucket', columns);

console.log(output);

if (isPaginated && format !== 'json' && format !== 'xml') {
printPaginationHint(nextToken);
}

printSuccess(context, { count: buckets.length });
}
32 changes: 27 additions & 5 deletions src/lib/iam/policies/list.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { getOAuthIAMConfig } from '@auth/iam.js';
import { listPolicies } from '@tigrisdata/iam';
import { failWithError } from '@utils/exit.js';
import { formatOutput } from '@utils/format.js';
import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js';
import { getFormat } from '@utils/options.js';
import { formatOutput, formatPaginatedOutput } from '@utils/format.js';
import {
msg,
printEmpty,
printPaginationHint,
printStart,
printSuccess,
} from '@utils/messages.js';
import { getFormat, getPaginationOptions } from '@utils/options.js';

const context = msg('iam policies', 'list');

export default async function list(options: Record<string, unknown>) {
printStart(context);

const format = getFormat(options);
const { limit, pageToken, isPaginated } = getPaginationOptions(options);

const iamConfig = await getOAuthIAMConfig(context);

const { data, error } = await listPolicies({
...(limit !== undefined ? { limit } : {}),
...(pageToken ? { paginationToken: pageToken } : {}),
config: {
sessionToken: iamConfig.sessionToken,
organizationId: iamConfig.organizationId,
Expand All @@ -41,16 +50,29 @@ export default async function list(options: Record<string, unknown>) {
updated: policy.updateDate,
}));

const output = formatOutput(policies, format!, 'policies', 'policy', [
const columns = [
{ key: 'id', header: 'ID' },
{ key: 'resource', header: 'Resource' },
{ key: 'name', header: 'Name' },
{ key: 'description', header: 'Description' },
{ key: 'attachments', header: 'Attachments' },
{ key: 'created', header: 'Created' },
{ key: 'updated', header: 'Updated' },
]);
];

const nextToken = data.paginationToken || undefined;

const output = isPaginated
? formatPaginatedOutput(policies, format!, 'policies', 'policy', columns, {
paginationToken: nextToken,
})
: formatOutput(policies, format!, 'policies', 'policy', columns);

console.log(output);

if (isPaginated && format !== 'json' && format !== 'xml') {
printPaginationHint(nextToken);
}

printSuccess(context, { count: policies.length });
}
36 changes: 31 additions & 5 deletions src/lib/objects/list.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { getStorageConfig } from '@auth/provider.js';
import { list } from '@tigrisdata/storage';
import { failWithError } from '@utils/exit.js';
import { formatOutput, formatSize } from '@utils/format.js';
import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js';
import { getFormat, getOption } from '@utils/options.js';
import {
formatOutput,
formatPaginatedOutput,
formatSize,
} from '@utils/format.js';
import {
msg,
printEmpty,
printPaginationHint,
printStart,
printSuccess,
} from '@utils/messages.js';
import { getFormat, getOption, getPaginationOptions } from '@utils/options.js';
import { parseAnyPath } from '@utils/path.js';

const context = msg('objects', 'list');
Expand All @@ -19,6 +29,7 @@ export default async function listObjects(options: Record<string, unknown>) {
'snapshotVersion',
'snapshot',
]);
const { limit, pageToken, isPaginated } = getPaginationOptions(options);

if (!bucketArg) {
failWithError(context, 'Bucket name is required');
Expand All @@ -33,6 +44,8 @@ export default async function listObjects(options: Record<string, unknown>) {
const { data, error } = await list({
prefix,
...(snapshotVersion ? { snapshotVersion } : {}),
...(limit !== undefined ? { limit } : {}),
...(pageToken ? { paginationToken: pageToken } : {}),
config: {
...config,
bucket,
Expand All @@ -54,12 +67,25 @@ export default async function listObjects(options: Record<string, unknown>) {
modified: item.lastModified,
}));

const output = formatOutput(objects, format!, 'objects', 'object', [
const columns = [
{ key: 'key', header: 'Key' },
{ key: 'size', header: 'Size' },
{ key: 'modified', header: 'Modified' },
]);
];

const nextToken = data.paginationToken || undefined;

const output = isPaginated
? formatPaginatedOutput(objects, format!, 'objects', 'object', columns, {
paginationToken: nextToken,
})
: formatOutput(objects, format!, 'objects', 'object', columns);

console.log(output);

if (isPaginated && format !== 'json' && format !== 'xml') {
printPaginationHint(nextToken);
}

printSuccess(context, { count: objects.length });
}
20 changes: 20 additions & 0 deletions src/specs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,11 @@ commands:
default: table
- name: forks-of
description: Only list buckets that are forks of the named source bucket
- name: limit
description: Maximum number of items to return per page
- name: page-token
description: Pagination token from a previous request to fetch the next page
alias: pt
# create
- name: create
description: Create a new bucket with optional access, tier, and location settings
Expand Down Expand Up @@ -1182,6 +1187,11 @@ commands:
- name: snapshot-version
description: Read from a specific bucket snapshot. Accepts a snapshot version string or any UNIX nanosecond-precision timestamp (e.g. 1765889000501544464)
alias: snapshot
- name: limit
description: Maximum number of items to return per page
- name: page-token
description: Pagination token from a previous request to fetch the next page
alias: pt
# get
- name: get
description: Download an object by key. Prints to stdout by default, or saves to a file with --output
Expand Down Expand Up @@ -1376,6 +1386,11 @@ commands:
description: Output format
options: [json, table, xml]
default: table
- name: limit
description: Maximum number of items to return per page
- name: page-token
description: Pagination token from a previous request to fetch the next page
alias: pt
- name: create
description: Create a new access key with the given name. Returns the key ID and secret (shown only once)
alias: c
Expand Down Expand Up @@ -1503,6 +1518,11 @@ commands:
description: Output format
options: [json, table, xml]
default: table
- name: limit
description: Maximum number of items to return per page
- name: page-token
description: Pagination token from a previous request to fetch the next page
alias: pt
- name: get
description: Show details for a policy including its document and attached users. If no ARN provided, shows interactive selection
alias: g
Expand Down
57 changes: 57 additions & 0 deletions src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,60 @@ export function formatOutput<T extends Record<string, unknown>>(
return formatTable(items, columns);
}
}

export interface PaginationMeta {
paginationToken?: string;
}

/**
* Format output with pagination metadata.
* JSON wraps items in an object with optional paginationToken.
* XML adds a <paginationToken> element inside the root.
* Table output is unchanged (pagination hint goes to stderr separately).
*/
export function formatPaginatedOutput<T extends Record<string, unknown>>(
items: T[],
format: string,
rootElement: string,
itemElement: string,
columns: TableColumn[],
pagination: PaginationMeta
): string {
switch (format) {
case 'json': {
const result: Record<string, unknown> = { items };
if (pagination.paginationToken) {
result.paginationToken = pagination.paginationToken;
}
return formatJson(result);
}
case 'xml':
return formatPaginatedXml(items, rootElement, itemElement, pagination);
default:
return formatTable(items, columns);
}
}

function formatPaginatedXml<T extends Record<string, unknown>>(
items: T[],
rootElement: string,
itemElement: string,
pagination: PaginationMeta
): string {
const lines = [`<${rootElement}>`];

items.forEach((item) => {
lines.push(` <${itemElement}>`);
lines.push(formatXmlObject(item, ' '));
lines.push(` </${itemElement}>`);
});

if (pagination.paginationToken) {
lines.push(
` <paginationToken>${escapeXml(pagination.paginationToken)}</paginationToken>`
);
}

lines.push(`</${rootElement}>`);
return lines.join('\n');
}
Loading