Skip to content

Add --all to repos list, cleanup pagination ux #580

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 21, 2025
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
38 changes: 29 additions & 9 deletions src/commands/repos/cmd-repos-list.mts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ const config: CliCommandConfig = {
flags: {
...commonFlags,
...outputFlags,
sort: {
type: 'string',
shortFlag: 's',
default: 'created_at',
description: 'Sorting option',
all: {
type: 'boolean',
default: false,
description:
'By default view shows the last n repos. This flag allows you to fetch the entire list. Will ignore --page and --perPage.',
},
direction: {
type: 'string',
Expand Down Expand Up @@ -56,6 +56,12 @@ const config: CliCommandConfig = {
default: 1,
description: 'Page number',
},
sort: {
type: 'string',
shortFlag: 's',
default: 'created_at',
description: 'Sorting option',
},
},
help: (command, config) => `
Usage
Expand Down Expand Up @@ -91,11 +97,17 @@ async function run(
parentName,
})

const { json, markdown } = cli.flags
const {
all,
direction = 'desc',
dryRun,
interactive,
json,
markdown,
org: orgFlag,
} = cli.flags
const outputKind = getOutputKind(json, markdown)

const { dryRun, interactive, org: orgFlag } = cli.flags

const [orgSlug] = await determineOrgSlug(
String(orgFlag || ''),
cli.input[0] || '',
Expand Down Expand Up @@ -132,6 +144,13 @@ async function run(
pass: 'ok',
fail: 'missing API token',
},
{
nook: true,
test: direction === 'asc' || direction === 'desc',
message: 'The --direction value must be "asc" or "desc"',
pass: 'ok',
fail: 'unexpected value',
},
)
if (!wasValidInput) {
return
Expand All @@ -143,7 +162,8 @@ async function run(
}

await handleListRepos({
direction: cli.flags['direction'] === 'asc' ? 'asc' : 'desc',
all: Boolean(all),
direction: direction === 'asc' ? 'asc' : 'desc',
orgSlug,
outputKind,
page: Number(cli.flags['page']) || 1,
Expand Down
1 change: 1 addition & 0 deletions src/commands/repos/cmd-repos-list.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('socket repos list', async () => {
- Permissions: repo:list

Options
--all By default view shows the last n repos. This flag allows you to fetch the entire list. Will ignore --page and --perPage.
--direction Direction option
--help Print this help
--interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting them to cancel/no.
Expand Down
58 changes: 58 additions & 0 deletions src/commands/repos/fetch-list-all-repos.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { handleApiCall } from '../../utils/api.mts'
import { setupSdk } from '../../utils/sdk.mts'

import type { CResult } from '../../types.mts'
import type { SocketSdkReturnType } from '@socketsecurity/sdk'

export async function fetchListAllRepos({
direction,
orgSlug,
sort,
}: {
direction: string
orgSlug: string
sort: string
}): Promise<CResult<SocketSdkReturnType<'getOrgRepoList'>['data']>> {
const sockSdkResult = await setupSdk()
if (!sockSdkResult.ok) {
return sockSdkResult
}
const sockSdk = sockSdkResult.data

const rows: SocketSdkReturnType<'getOrgRepoList'>['data']['results'] = []
let protection = 0
let nextPage = 0
while (nextPage >= 0) {
if (++protection > 100) {
return {
ok: false,
message: 'Infinite loop detected',
cause: `Either there are over 100 pages of results or the fetch has run into an infinite loop. Breaking it off now. nextPage=${nextPage}`,
}
}
// eslint-disable-next-line no-await-in-loop
const result = await handleApiCall(
sockSdk.getOrgRepoList(orgSlug, {
sort,
direction,
per_page: String(100), // max
page: String(nextPage),
}),
'list of repositories',
)
if (!result.ok) {
return result
}

result.data.results.forEach(row => rows.push(row))
nextPage = result.data.nextPage ?? -1
}

return {
ok: true,
data: {
results: rows,
nextPage: null,
},
}
}
40 changes: 31 additions & 9 deletions src/commands/repos/handle-list-repos.mts
Original file line number Diff line number Diff line change
@@ -1,30 +1,52 @@
import { fetchListAllRepos } from './fetch-list-all-repos.mts'
import { fetchListRepos } from './fetch-list-repos.mts'
import { outputListRepos } from './output-list-repos.mts'

import type { OutputKind } from '../../types.mts'

export async function handleListRepos({
all,
direction,
orgSlug,
outputKind,
page,
per_page,
sort,
}: {
direction: string
all: boolean
direction: 'asc' | 'desc'
orgSlug: string
outputKind: OutputKind
page: number
per_page: number
sort: string
}): Promise<void> {
const data = await fetchListRepos({
direction,
orgSlug,
page,
per_page,
sort,
})
if (all) {
const data = await fetchListAllRepos({ direction, orgSlug, sort })

await outputListRepos(data, outputKind)
await outputListRepos(data, outputKind, 0, 0, sort, Infinity, direction)
} else {
const data = await fetchListRepos({
direction,
orgSlug,
page,
per_page,
sort,
})

if (!data.ok) {
await outputListRepos(data, outputKind, 0, 0, '', 0, direction)
} else {
// Note: nextPage defaults to 0, is null when there's no next page
await outputListRepos(
data,
outputKind,
page,
data.data.nextPage,
sort,
per_page,
direction,
)
}
}
}
39 changes: 38 additions & 1 deletion src/commands/repos/output-list-repos.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,45 @@ import type { SocketSdkReturnType } from '@socketsecurity/sdk'
export async function outputListRepos(
result: CResult<SocketSdkReturnType<'getOrgRepoList'>['data']>,
outputKind: OutputKind,
page: number,
nextPage: number | null,
sort: string,
perPage: number,
direction: 'asc' | 'desc',
): Promise<void> {
if (!result.ok) {
process.exitCode = result.code ?? 1
}

if (outputKind === 'json') {
logger.log(serializeResultJson(result))
if (result.ok) {
logger.log(
serializeResultJson({
ok: true,
data: {
data: result.data,
direction,
nextPage: nextPage ?? 0,
page,
perPage,
sort,
},
}),
)
} else {
logger.log(serializeResultJson(result))
}
return
}
if (!result.ok) {
logger.fail(failMsgWithBadge(result.message, result.cause))
return
}

logger.log(
`Result page: ${page}, results per page: ${perPage === Infinity ? 'all' : perPage}, sorted by: ${sort}, direction: ${direction}`,
)

const options = {
columns: [
{ field: 'id', name: colors.magenta('ID') },
Expand All @@ -38,4 +63,16 @@ export async function outputListRepos(
}

logger.log(chalkTable(options, result.data.results))
if (nextPage) {
logger.info(
`This is page ${page}. Server indicated there are more results available on page ${nextPage}...`,
)
logger.info(`(Hint: you can use \`socket repos list --page ${nextPage}\`)`)
} else if (perPage === Infinity) {
logger.info(`This should be the entire list available on the server.`)
} else {
logger.info(
`This is page ${page}. Server indicated this is the last page with results.`,
)
}
}
Loading