From 4c02fb8dd6e6afd819adeaa70199e8f6b74ed05e Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 8 Apr 2026 18:39:33 +0200 Subject: [PATCH 1/6] fix: always show pagination token when more results exist Remove the isPaginated gate from output decisions. The pagination token and hint are now shown whenever the response indicates more items exist, regardless of whether --limit or --page-token were explicitly passed. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/access-keys/list.ts | 6 +++--- src/lib/buckets/list.ts | 6 +++--- src/lib/iam/policies/list.ts | 6 +++--- src/lib/objects/list.ts | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lib/access-keys/list.ts b/src/lib/access-keys/list.ts index 261fa25..569d47f 100644 --- a/src/lib/access-keys/list.ts +++ b/src/lib/access-keys/list.ts @@ -17,7 +17,7 @@ export default async function list(options: Record) { printStart(context); const format = getFormat(options); - const { limit, pageToken, isPaginated } = getPaginationOptions(options); + const { limit, pageToken } = getPaginationOptions(options); const config = await getIAMConfig(context); @@ -52,7 +52,7 @@ export default async function list(options: Record) { const nextToken = data.paginationToken || undefined; - const output = isPaginated + const output = nextToken ? formatPaginatedOutput(keys, format!, 'keys', 'key', columns, { paginationToken: nextToken, }) @@ -60,7 +60,7 @@ export default async function list(options: Record) { console.log(output); - if (isPaginated && format !== 'json' && format !== 'xml') { + if (format !== 'json' && format !== 'xml') { printPaginationHint(nextToken); } diff --git a/src/lib/buckets/list.ts b/src/lib/buckets/list.ts index f59b0b8..5ba35ec 100644 --- a/src/lib/buckets/list.ts +++ b/src/lib/buckets/list.ts @@ -28,7 +28,7 @@ export default async function list(options: Record) { } const { data, error } = await listBuckets({ - ...(forksOf || !isPaginated + ...(forksOf ? {} : { ...(limit !== undefined ? { limit } : {}), @@ -99,7 +99,7 @@ export default async function list(options: Record) { const nextToken = data.paginationToken || undefined; - const output = isPaginated + const output = nextToken ? formatPaginatedOutput(buckets, format!, 'buckets', 'bucket', columns, { paginationToken: nextToken, }) @@ -107,7 +107,7 @@ export default async function list(options: Record) { console.log(output); - if (isPaginated && format !== 'json' && format !== 'xml') { + if (format !== 'json' && format !== 'xml') { printPaginationHint(nextToken); } diff --git a/src/lib/iam/policies/list.ts b/src/lib/iam/policies/list.ts index 5004e0e..7e19662 100644 --- a/src/lib/iam/policies/list.ts +++ b/src/lib/iam/policies/list.ts @@ -17,7 +17,7 @@ export default async function list(options: Record) { printStart(context); const format = getFormat(options); - const { limit, pageToken, isPaginated } = getPaginationOptions(options); + const { limit, pageToken } = getPaginationOptions(options); const iamConfig = await getOAuthIAMConfig(context); @@ -62,7 +62,7 @@ export default async function list(options: Record) { const nextToken = data.paginationToken || undefined; - const output = isPaginated + const output = nextToken ? formatPaginatedOutput(policies, format!, 'policies', 'policy', columns, { paginationToken: nextToken, }) @@ -70,7 +70,7 @@ export default async function list(options: Record) { console.log(output); - if (isPaginated && format !== 'json' && format !== 'xml') { + if (format !== 'json' && format !== 'xml') { printPaginationHint(nextToken); } diff --git a/src/lib/objects/list.ts b/src/lib/objects/list.ts index 5234c0c..dda2606 100644 --- a/src/lib/objects/list.ts +++ b/src/lib/objects/list.ts @@ -29,7 +29,7 @@ export default async function listObjects(options: Record) { 'snapshotVersion', 'snapshot', ]); - const { limit, pageToken, isPaginated } = getPaginationOptions(options); + const { limit, pageToken } = getPaginationOptions(options); if (!bucketArg) { failWithError(context, 'Bucket name is required'); @@ -75,7 +75,7 @@ export default async function listObjects(options: Record) { const nextToken = data.paginationToken || undefined; - const output = isPaginated + const output = nextToken ? formatPaginatedOutput(objects, format!, 'objects', 'object', columns, { paginationToken: nextToken, }) @@ -83,7 +83,7 @@ export default async function listObjects(options: Record) { console.log(output); - if (isPaginated && format !== 'json' && format !== 'xml') { + if (format !== 'json' && format !== 'xml') { printPaginationHint(nextToken); } From ceed8656280119fb02870ca2605a9dbc83fbcb00 Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 8 Apr 2026 18:42:17 +0200 Subject: [PATCH 2/6] fix: add pagination support to ls command The ls command also uses list() and listBuckets() from the SDK but was missing pagination flags and token display. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/ls.ts | 51 +++++++++++++++++++++++++++++++++++++++++++------- src/specs.yaml | 5 +++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/lib/ls.ts b/src/lib/ls.ts index 5451d8d..141b482 100644 --- a/src/lib/ls.ts +++ b/src/lib/ls.ts @@ -1,8 +1,13 @@ import { getStorageConfig } from '@auth/provider.js'; import { list, listBuckets } from '@tigrisdata/storage'; import { exitWithError } from '@utils/exit.js'; -import { formatOutput, formatSize } from '@utils/format.js'; -import { getFormat, getOption } from '@utils/options.js'; +import { + formatOutput, + formatPaginatedOutput, + formatSize, +} from '@utils/format.js'; +import { printPaginationHint } from '@utils/messages.js'; +import { getFormat, getOption, getPaginationOptions } from '@utils/options.js'; import { parseAnyPath } from '@utils/path.js'; export default async function ls(options: Record) { @@ -13,11 +18,16 @@ export default async function ls(options: Record) { 'snapshot', ]); const format = getFormat(options); + const { limit, pageToken } = getPaginationOptions(options); if (!pathString) { // No path provided, list all buckets const config = await getStorageConfig(); - const { data, error } = await listBuckets({ config }); + const { data, error } = await listBuckets({ + ...(limit !== undefined ? { limit } : {}), + ...(pageToken ? { paginationToken: pageToken } : {}), + config, + }); if (error) { exitWithError(error); @@ -28,12 +38,25 @@ export default async function ls(options: Record) { 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 = nextToken + ? formatPaginatedOutput(buckets, format!, 'buckets', 'bucket', columns, { + paginationToken: nextToken, + }) + : formatOutput(buckets, format!, 'buckets', 'bucket', columns); console.log(output); + + if (format !== 'json' && format !== 'xml') { + printPaginationHint(nextToken); + } + return; } @@ -51,6 +74,8 @@ export default async function ls(options: Record) { const { data, error } = await list({ prefix, ...(snapshotVersion ? { snapshotVersion } : {}), + ...(limit !== undefined ? { limit } : {}), + ...(pageToken ? { paginationToken: pageToken } : {}), config: { ...config, bucket, @@ -84,11 +109,23 @@ export default async function ls(options: Record) { item.key !== '' && arr.findIndex((i) => i.key === item.key) === index ); - 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 = nextToken + ? formatPaginatedOutput(objects, format!, 'objects', 'object', columns, { + paginationToken: nextToken, + }) + : formatOutput(objects, format!, 'objects', 'object', columns); console.log(output); + + if (format !== 'json' && format !== 'xml') { + printPaginationHint(nextToken); + } } diff --git a/src/specs.yaml b/src/specs.yaml index e32655a..504058b 100644 --- a/src/specs.yaml +++ b/src/specs.yaml @@ -292,6 +292,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 # mk - name: mk From 304be86d377c5d1d0a15b801f26cf433f6680112 Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 8 Apr 2026 18:50:59 +0200 Subject: [PATCH 3/6] fix: use consistent JSON schema for all list commands Always use formatPaginatedOutput so JSON output is consistently { "items": [...] } with optional paginationToken, rather than switching between a bare array and wrapped object based on server response. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/access-keys/list.ts | 10 ++++------ src/lib/buckets/list.ts | 13 ++++++++----- src/lib/iam/policies/list.ts | 15 +++++++++------ src/lib/ls.ts | 32 +++++++++++++++++--------------- src/lib/objects/list.ts | 19 +++++++++---------- 5 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/lib/access-keys/list.ts b/src/lib/access-keys/list.ts index 569d47f..6e6b71d 100644 --- a/src/lib/access-keys/list.ts +++ b/src/lib/access-keys/list.ts @@ -1,7 +1,7 @@ import { getIAMConfig } from '@auth/iam.js'; import { listAccessKeys } from '@tigrisdata/iam'; import { failWithError } from '@utils/exit.js'; -import { formatOutput, formatPaginatedOutput } from '@utils/format.js'; +import { formatPaginatedOutput } from '@utils/format.js'; import { msg, printEmpty, @@ -52,11 +52,9 @@ export default async function list(options: Record) { const nextToken = data.paginationToken || undefined; - const output = nextToken - ? formatPaginatedOutput(keys, format!, 'keys', 'key', columns, { - paginationToken: nextToken, - }) - : formatOutput(keys, format!, 'keys', 'key', columns); + const output = formatPaginatedOutput(keys, format!, 'keys', 'key', columns, { + paginationToken: nextToken, + }); console.log(output); diff --git a/src/lib/buckets/list.ts b/src/lib/buckets/list.ts index 5ba35ec..734365f 100644 --- a/src/lib/buckets/list.ts +++ b/src/lib/buckets/list.ts @@ -99,11 +99,14 @@ export default async function list(options: Record) { const nextToken = data.paginationToken || undefined; - const output = nextToken - ? formatPaginatedOutput(buckets, format!, 'buckets', 'bucket', columns, { - paginationToken: nextToken, - }) - : formatOutput(buckets, format!, 'buckets', 'bucket', columns); + const output = formatPaginatedOutput( + buckets, + format!, + 'buckets', + 'bucket', + columns, + { paginationToken: nextToken } + ); console.log(output); diff --git a/src/lib/iam/policies/list.ts b/src/lib/iam/policies/list.ts index 7e19662..15ba76b 100644 --- a/src/lib/iam/policies/list.ts +++ b/src/lib/iam/policies/list.ts @@ -1,7 +1,7 @@ import { getOAuthIAMConfig } from '@auth/iam.js'; import { listPolicies } from '@tigrisdata/iam'; import { failWithError } from '@utils/exit.js'; -import { formatOutput, formatPaginatedOutput } from '@utils/format.js'; +import { formatPaginatedOutput } from '@utils/format.js'; import { msg, printEmpty, @@ -62,11 +62,14 @@ export default async function list(options: Record) { const nextToken = data.paginationToken || undefined; - const output = nextToken - ? formatPaginatedOutput(policies, format!, 'policies', 'policy', columns, { - paginationToken: nextToken, - }) - : formatOutput(policies, format!, 'policies', 'policy', columns); + const output = formatPaginatedOutput( + policies, + format!, + 'policies', + 'policy', + columns, + { paginationToken: nextToken } + ); console.log(output); diff --git a/src/lib/ls.ts b/src/lib/ls.ts index 141b482..2bfb789 100644 --- a/src/lib/ls.ts +++ b/src/lib/ls.ts @@ -1,11 +1,7 @@ import { getStorageConfig } from '@auth/provider.js'; import { list, listBuckets } from '@tigrisdata/storage'; import { exitWithError } from '@utils/exit.js'; -import { - formatOutput, - formatPaginatedOutput, - formatSize, -} from '@utils/format.js'; +import { formatPaginatedOutput, formatSize } from '@utils/format.js'; import { printPaginationHint } from '@utils/messages.js'; import { getFormat, getOption, getPaginationOptions } from '@utils/options.js'; import { parseAnyPath } from '@utils/path.js'; @@ -45,11 +41,14 @@ export default async function ls(options: Record) { const nextToken = data.paginationToken || undefined; - const output = nextToken - ? formatPaginatedOutput(buckets, format!, 'buckets', 'bucket', columns, { - paginationToken: nextToken, - }) - : formatOutput(buckets, format!, 'buckets', 'bucket', columns); + const output = formatPaginatedOutput( + buckets, + format!, + 'buckets', + 'bucket', + columns, + { paginationToken: nextToken } + ); console.log(output); @@ -117,11 +116,14 @@ export default async function ls(options: Record) { const nextToken = data.paginationToken || undefined; - const output = nextToken - ? formatPaginatedOutput(objects, format!, 'objects', 'object', columns, { - paginationToken: nextToken, - }) - : formatOutput(objects, format!, 'objects', 'object', columns); + const output = formatPaginatedOutput( + objects, + format!, + 'objects', + 'object', + columns, + { paginationToken: nextToken } + ); console.log(output); diff --git a/src/lib/objects/list.ts b/src/lib/objects/list.ts index dda2606..f9ff4b0 100644 --- a/src/lib/objects/list.ts +++ b/src/lib/objects/list.ts @@ -1,11 +1,7 @@ import { getStorageConfig } from '@auth/provider.js'; import { list } from '@tigrisdata/storage'; import { failWithError } from '@utils/exit.js'; -import { - formatOutput, - formatPaginatedOutput, - formatSize, -} from '@utils/format.js'; +import { formatPaginatedOutput, formatSize } from '@utils/format.js'; import { msg, printEmpty, @@ -75,11 +71,14 @@ export default async function listObjects(options: Record) { const nextToken = data.paginationToken || undefined; - const output = nextToken - ? formatPaginatedOutput(objects, format!, 'objects', 'object', columns, { - paginationToken: nextToken, - }) - : formatOutput(objects, format!, 'objects', 'object', columns); + const output = formatPaginatedOutput( + objects, + format!, + 'objects', + 'object', + columns, + { paginationToken: nextToken } + ); console.log(output); From c1edeabe608b65f6be367a1a99bb7a638144619a Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 8 Apr 2026 19:56:19 +0200 Subject: [PATCH 4/6] feat: add pagination support to snapshots list The storage SDK now supports pagination in listBucketSnapshots. Update the snapshots list command to accept --limit and --page-token flags and display the pagination token when more results exist. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/snapshots/list.ts | 43 +++++++++++++++++++++++++++++++-------- src/specs.yaml | 5 +++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/lib/snapshots/list.ts b/src/lib/snapshots/list.ts index 4875ef6..3cfbf9c 100644 --- a/src/lib/snapshots/list.ts +++ b/src/lib/snapshots/list.ts @@ -1,9 +1,15 @@ import { getStorageConfig } from '@auth/provider.js'; import { listBucketSnapshots } 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 { 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('snapshots', 'list'); @@ -12,6 +18,7 @@ export default async function list(options: Record) { const name = getOption(options, ['name']); const format = getFormat(options); + const { limit, pageToken } = getPaginationOptions(options); if (!name) { failWithError(context, 'Bucket name is required'); @@ -19,29 +26,49 @@ export default async function list(options: Record) { const config = await getStorageConfig(); - const { data, error } = await listBucketSnapshots(name, { config }); + const { data, error } = await listBucketSnapshots(name, { + ...(limit !== undefined ? { limit } : {}), + ...(pageToken ? { paginationToken: pageToken } : {}), + config, + }); if (error) { failWithError(context, error); } - if (!data || data.length === 0) { + if (!data.snapshots || data.snapshots.length === 0) { printEmpty(context); return; } - const snapshots = data.map((snapshot) => ({ + const snapshots = data.snapshots.map((snapshot) => ({ name: snapshot.name || '', version: snapshot.version || '', created: snapshot.creationDate, })); - const output = formatOutput(snapshots, format!, 'snapshots', 'snapshot', [ + const columns = [ { key: 'name', header: 'Name' }, { key: 'version', header: 'Version' }, { key: 'created', header: 'Created' }, - ]); + ]; + + const nextToken = data.paginationToken || undefined; + + const output = formatPaginatedOutput( + snapshots, + format!, + 'snapshots', + 'snapshot', + columns, + { paginationToken: nextToken } + ); console.log(output); + + if (format !== 'json' && format !== 'xml') { + printPaginationHint(nextToken); + } + printSuccess(context, { count: snapshots.length }); } diff --git a/src/specs.yaml b/src/specs.yaml index 504058b..0692b7d 100644 --- a/src/specs.yaml +++ b/src/specs.yaml @@ -1120,6 +1120,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 # take - name: take description: Take a new snapshot of the bucket's current state. Optionally provide a name for the snapshot From c7af79206b20df52fa67e58c4047dae7bc23624a Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 8 Apr 2026 19:59:08 +0200 Subject: [PATCH 5/6] chore: update @tigrisdata/storage with snapshot pagination support Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f1cb28..fc5f849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@aws-sdk/credential-providers": "^3.1024.0", "@smithy/shared-ini-file-loader": "^4.4.7", "@tigrisdata/iam": "^1.4.1", - "@tigrisdata/storage": "^2.16.2", + "@tigrisdata/storage": "^3.0.0", "commander": "^14.0.3", "enquirer": "^2.4.1", "jose": "^6.2.2", @@ -4015,9 +4015,9 @@ } }, "node_modules/@tigrisdata/storage": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@tigrisdata/storage/-/storage-2.16.2.tgz", - "integrity": "sha512-O17sUXp+8o5d+fjUwDYO4RhdRloJ3KazM+ICG7pbo682aKX5ROpnYH8zJn3qpzLn49CJy9TpdyknjuxrTX/aLA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tigrisdata/storage/-/storage-3.0.0.tgz", + "integrity": "sha512-Rhw+aEOpl2bcgDhIymAguX2m178TYdco+lmX+zxYHw+P9jX8v4euwnZwRSb/+YwqmEawhBeapdNkCgIsBIVZ8g==", "license": "MIT", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", diff --git a/package.json b/package.json index d9b0bcc..4532e17 100644 --- a/package.json +++ b/package.json @@ -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.16.2", + "@tigrisdata/storage": "^3.0.0", "commander": "^14.0.3", "enquirer": "^2.4.1", "jose": "^6.2.2", From 32ee29a6c07873e958c6c3c2df89f2691fcc4bd4 Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 8 Apr 2026 20:03:11 +0200 Subject: [PATCH 6/6] fix: update fork detection to use new SDK forkInfo API The updated @tigrisdata/storage SDK replaced hasForks and sourceBucketName with forkInfo.hasChildren and forkInfo.parents[]. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/buckets/list.ts | 7 +++++-- src/lib/forks/list.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/lib/buckets/list.ts b/src/lib/buckets/list.ts index 734365f..243196f 100644 --- a/src/lib/buckets/list.ts +++ b/src/lib/buckets/list.ts @@ -57,7 +57,7 @@ export default async function list(options: Record) { failWithError(context, infoError); } - if (!bucketInfo.hasForks) { + if (!bucketInfo.forkInfo?.hasChildren) { printEmpty(context); return; } @@ -67,7 +67,10 @@ export default async function list(options: Record) { for (const bucket of data.buckets) { if (bucket.name === forksOf) continue; const { data: info } = await getBucketInfo(bucket.name, { config }); - if (info?.sourceBucketName === forksOf) { + const isChildOf = info?.forkInfo?.parents?.some( + (p) => p.bucketName === forksOf + ); + if (isChildOf) { forks.push({ name: bucket.name, created: bucket.creationDate }); } } diff --git a/src/lib/forks/list.ts b/src/lib/forks/list.ts index 5dee405..62a51cc 100644 --- a/src/lib/forks/list.ts +++ b/src/lib/forks/list.ts @@ -28,7 +28,7 @@ export default async function list(options: Record) { failWithError(context, infoError); } - if (!bucketInfo.hasForks) { + if (!bucketInfo.forkInfo?.hasChildren) { printEmpty(context); return; } @@ -47,7 +47,10 @@ export default async function list(options: Record) { if (bucket.name === name) continue; const { data: info } = await getBucketInfo(bucket.name, { config }); - if (info?.sourceBucketName === name) { + const isChildOf = info?.forkInfo?.parents?.some( + (p) => p.bucketName === name + ); + if (isChildOf) { forks.push({ name: bucket.name, created: bucket.creationDate,