From 307285293af862ef8f4f7652c6a9b61816c0a8b4 Mon Sep 17 00:00:00 2001 From: Simone Carletti Date: Mon, 25 May 2026 11:13:43 +0200 Subject: [PATCH 1/4] Show pagination hint on list command output --- internal/cli/ai_context.md | 9 +++ internal/cli/analytics.go | 4 +- internal/cli/billing.go | 2 +- internal/cli/certificates.go | 2 +- internal/cli/contacts.go | 2 +- internal/cli/domains.go | 2 +- internal/cli/domains_ds_records.go | 2 +- internal/cli/domains_email_forwards.go | 2 +- internal/cli/domains_pushes.go | 2 +- internal/cli/list_output.go | 24 ++++++++ internal/cli/output_test.go | 79 +++++++++++++++++++++++++ internal/cli/registrar_registrant.go | 2 +- internal/cli/services.go | 4 +- internal/cli/templates.go | 2 +- internal/cli/templates_records.go | 2 +- internal/cli/tlds.go | 2 +- internal/cli/zones.go | 2 +- internal/cli/zones_records.go | 2 +- internal/output/format.go | 31 ++++++++++ internal/output/format_test.go | 82 ++++++++++++++++++++++++++ 20 files changed, 242 insertions(+), 17 deletions(-) create mode 100644 internal/cli/list_output.go diff --git a/internal/cli/ai_context.md b/internal/cli/ai_context.md index 7617a94..7eb725c 100644 --- a/internal/cli/ai_context.md +++ b/internal/cli/ai_context.md @@ -26,6 +26,15 @@ These flags work with any command: When scripting or parsing output programmatically, always use `--json`. +## Pagination + +List commands are paginated and return only the first page by default (30 items). +The `--json` response includes a `pagination` object (`current_page`, `per_page`, +`total_pages`, `total_entries`) — inspect it to decide whether more results exist. In +the default table output, a `Showing X of Y ...` hint is written to stderr when more +pages are available. To retrieve everything in one call, pass `--all` (where supported); +otherwise page through with `--page ` and `--per-page `. + ## Common Workflows ### List all DNS records for a zone diff --git a/internal/cli/analytics.go b/internal/cli/analytics.go index b7f3978..f9e0af6 100644 --- a/internal/cli/analytics.go +++ b/internal/cli/analytics.go @@ -163,11 +163,11 @@ func newAnalyticsQueryCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&analyticsOutput{ + return f.Printer(cmd).PrintList(&analyticsOutput{ Data: resp.Data, Pagination: resp.Pagination, Groupings: effectiveGroupings, - }) + }, pageHint(cmd, resp.Pagination, len(resp.Data), "analytics rows")) }, } diff --git a/internal/cli/billing.go b/internal/cli/billing.go index 0c0918a..aaf3fe5 100644 --- a/internal/cli/billing.go +++ b/internal/cli/billing.go @@ -73,7 +73,7 @@ func newBillingChargesCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&chargeList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&chargeList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "charges")) }, } diff --git a/internal/cli/certificates.go b/internal/cli/certificates.go index bc96ced..433321e 100644 --- a/internal/cli/certificates.go +++ b/internal/cli/certificates.go @@ -198,7 +198,7 @@ func newCertsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&certList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&certList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "certificates")) }, } diff --git a/internal/cli/contacts.go b/internal/cli/contacts.go index b7f779b..43f7c4a 100644 --- a/internal/cli/contacts.go +++ b/internal/cli/contacts.go @@ -144,7 +144,7 @@ func newContactsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&contactList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&contactList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "contacts")) }, } diff --git a/internal/cli/domains.go b/internal/cli/domains.go index b398b62..ac81ce8 100644 --- a/internal/cli/domains.go +++ b/internal/cli/domains.go @@ -151,7 +151,7 @@ func newDomainsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&domainList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&domainList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "domains")) }, } diff --git a/internal/cli/domains_ds_records.go b/internal/cli/domains_ds_records.go index dee6976..bcbb116 100644 --- a/internal/cli/domains_ds_records.go +++ b/internal/cli/domains_ds_records.go @@ -100,7 +100,7 @@ func newDsRecordsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&dsRecordList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&dsRecordList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "DS records")) }, } } diff --git a/internal/cli/domains_email_forwards.go b/internal/cli/domains_email_forwards.go index 3cac9b0..24ad000 100644 --- a/internal/cli/domains_email_forwards.go +++ b/internal/cli/domains_email_forwards.go @@ -97,7 +97,7 @@ func newEmailForwardsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&emailForwardList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&emailForwardList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "email forwards")) }, } } diff --git a/internal/cli/domains_pushes.go b/internal/cli/domains_pushes.go index 472cad6..bae88d4 100644 --- a/internal/cli/domains_pushes.go +++ b/internal/cli/domains_pushes.go @@ -96,7 +96,7 @@ func newPushesListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&pushList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&pushList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "domain pushes")) }, } } diff --git a/internal/cli/list_output.go b/internal/cli/list_output.go new file mode 100644 index 0000000..4ac2632 --- /dev/null +++ b/internal/cli/list_output.go @@ -0,0 +1,24 @@ +package cli + +import ( + "github.com/dnsimple/cli/internal/output" + "github.com/dnsimple/dnsimple-go/v8/dnsimple" + "github.com/spf13/cobra" +) + +// pageHint builds the table pagination discovery hint from an API pagination +// response. It returns nil when pagination is absent (for example when --all +// already fetched every page), which tells the printer not to emit a hint. +func pageHint(cmd *cobra.Command, pg *dnsimple.Pagination, shown int, noun string) *output.PageInfo { + if pg == nil { + return nil + } + return &output.PageInfo{ + Noun: noun, + Shown: shown, + CurrentPage: pg.CurrentPage, + TotalPages: pg.TotalPages, + TotalEntries: pg.TotalEntries, + CanFetchAll: cmd.Flags().Lookup("all") != nil, + } +} diff --git a/internal/cli/output_test.go b/internal/cli/output_test.go index 08d1393..533f12a 100644 --- a/internal/cli/output_test.go +++ b/internal/cli/output_test.go @@ -183,6 +183,85 @@ func TestDomainsListUsesTemplateOutputOnUnderlyingResourceList(t *testing.T) { assert.Zero(t, stderr.Len()) } +func TestRecordsListShowsPaginationHintOnTable(t *testing.T) { + client, cfg := testCLIClient(t, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/v2/1950/zones/example.com/records", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{"data":[{"id":1,"type":"A","name":"","content":"1.2.3.4","ttl":3600},{"id":2,"type":"A","name":"www","content":"1.2.3.5","ttl":3600}],"pagination":{"current_page":1,"per_page":30,"total_entries":142,"total_pages":5}}`) + }) + + f := cmdutil.NewFactory("test") + f.Client = func() (*dnsimple.Client, error) { return client, nil } + f.Config = func() (*config.Config, error) { return cfg, nil } + f.AccountID = func() (string, error) { return "1950", nil } + + cmd := newRecordsListCmd(f) + var stdout, stderr bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stderr) + + err := cmd.RunE(cmd, []string{"example.com"}) + if !assert.NoError(t, err) { + return + } + + assert.Contains(t, stderr.String(), "Showing 2 of 142 records (page 1 of 5)") + assert.Contains(t, stderr.String(), "--all") + assert.Contains(t, stdout.String(), "1.2.3.4") +} + +func TestRecordsListJSONIncludesPaginationWithoutHint(t *testing.T) { + client, cfg := testCLIClient(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{"data":[{"id":1,"type":"A","name":"","content":"1.2.3.4","ttl":3600}],"pagination":{"current_page":1,"per_page":30,"total_entries":142,"total_pages":5}}`) + }) + + f := cmdutil.NewFactory("test") + f.Client = func() (*dnsimple.Client, error) { return client, nil } + f.Config = func() (*config.Config, error) { return cfg, nil } + f.AccountID = func() (string, error) { return "1950", nil } + f.Flags.JSON = true + + cmd := newRecordsListCmd(f) + var stdout, stderr bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stderr) + + err := cmd.RunE(cmd, []string{"example.com"}) + if !assert.NoError(t, err) { + return + } + + assert.Zero(t, stderr.Len()) + assert.Contains(t, stdout.String(), `"pagination"`) + assert.Contains(t, stdout.String(), `"total_entries": 142`) +} + +func TestRecordsListNoHintOnSinglePage(t *testing.T) { + client, cfg := testCLIClient(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{"data":[{"id":1,"type":"A","name":"","content":"1.2.3.4","ttl":3600}],"pagination":{"current_page":1,"per_page":30,"total_entries":1,"total_pages":1}}`) + }) + + f := cmdutil.NewFactory("test") + f.Client = func() (*dnsimple.Client, error) { return client, nil } + f.Config = func() (*config.Config, error) { return cfg, nil } + f.AccountID = func() (string, error) { return "1950", nil } + + cmd := newRecordsListCmd(f) + var stdout, stderr bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stderr) + + err := cmd.RunE(cmd, []string{"example.com"}) + if !assert.NoError(t, err) { + return + } + + assert.Zero(t, stderr.Len()) + assert.Contains(t, stdout.String(), "1.2.3.4") +} + func testCLIClient(t *testing.T, handler http.HandlerFunc) (*dnsimple.Client, *config.Config) { t.Helper() diff --git a/internal/cli/registrar_registrant.go b/internal/cli/registrar_registrant.go index 441b145..bb82244 100644 --- a/internal/cli/registrar_registrant.go +++ b/internal/cli/registrar_registrant.go @@ -107,7 +107,7 @@ func newRegistrantChangeListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(®istrantChangeList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(®istrantChangeList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "registrant changes")) }, } diff --git a/internal/cli/services.go b/internal/cli/services.go index ed386c8..d35a5d6 100644 --- a/internal/cli/services.go +++ b/internal/cli/services.go @@ -91,7 +91,7 @@ func newServicesListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&serviceList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&serviceList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "services")) }, } } @@ -138,7 +138,7 @@ func newServicesAppliedCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&serviceList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&serviceList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "applied services")) }, } } diff --git a/internal/cli/templates.go b/internal/cli/templates.go index 0c280ce..02573d0 100644 --- a/internal/cli/templates.go +++ b/internal/cli/templates.go @@ -96,7 +96,7 @@ func newTemplatesListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&templateList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&templateList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "templates")) }, } } diff --git a/internal/cli/templates_records.go b/internal/cli/templates_records.go index a95823a..8717eae 100644 --- a/internal/cli/templates_records.go +++ b/internal/cli/templates_records.go @@ -98,7 +98,7 @@ func newTemplateRecordsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&templateRecordList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&templateRecordList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "template records")) }, } } diff --git a/internal/cli/tlds.go b/internal/cli/tlds.go index 0e1c53b..e6c0825 100644 --- a/internal/cli/tlds.go +++ b/internal/cli/tlds.go @@ -130,7 +130,7 @@ func newTldsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&tldList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&tldList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "TLDs")) }, } diff --git a/internal/cli/zones.go b/internal/cli/zones.go index 83fcadd..b61cbf5 100644 --- a/internal/cli/zones.go +++ b/internal/cli/zones.go @@ -144,7 +144,7 @@ func newZonesListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&zoneList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&zoneList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "zones")) }, } diff --git a/internal/cli/zones_records.go b/internal/cli/zones_records.go index aefa5b5..0ead9b5 100644 --- a/internal/cli/zones_records.go +++ b/internal/cli/zones_records.go @@ -166,7 +166,7 @@ func newRecordsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - return f.Printer(cmd).Print(&recordList{Data: resp.Data, Pagination: resp.Pagination}) + return f.Printer(cmd).PrintList(&recordList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "records")) }, } diff --git a/internal/output/format.go b/internal/output/format.go index 488e4ad..10afe09 100644 --- a/internal/output/format.go +++ b/internal/output/format.go @@ -24,6 +24,17 @@ type Formattable interface { TemplateData() any } +// PageInfo describes pagination state for the table discovery hint. It is +// intentionally SDK-free so the output package stays decoupled from the API client. +type PageInfo struct { + Noun string // resource label, e.g. "records" + Shown int // items on the current page + CurrentPage int + TotalPages int + TotalEntries int + CanFetchAll bool // command exposes an --all flag +} + // Printer handles output rendering. type Printer struct { Writer io.Writer @@ -56,6 +67,26 @@ func (p *Printer) Print(data Formattable) error { } } +// PrintList renders a paginated list. For table output spanning more than one +// page it writes a discovery hint to the error stream above the table, so a user +// (or AI agent) can see that more results exist and how to reach them. JSON and +// template output are left untouched: JSON already embeds the pagination object. +func (p *Printer) PrintList(data Formattable, info *PageInfo) error { + if p.Format == FormatTable && info != nil && info.TotalPages > 1 { + p.writePageHint(info) + } + return p.Print(data) +} + +func (p *Printer) writePageHint(info *PageInfo) { + nav := "Pass --page or --per-page to see more." + if info.CanFetchAll { + nav = "Pass --all to fetch every page, or --page /--per-page to navigate." + } + fmt.Fprintf(p.ErrWriter, "Showing %d of %d %s (page %d of %d). %s\n", + info.Shown, info.TotalEntries, info.Noun, info.CurrentPage, info.TotalPages, nav) +} + func (p *Printer) printJSON(data Formattable) error { enc := json.NewEncoder(p.Writer) enc.SetIndent("", " ") diff --git a/internal/output/format_test.go b/internal/output/format_test.go index 4481127..0391fef 100644 --- a/internal/output/format_test.go +++ b/internal/output/format_test.go @@ -89,3 +89,85 @@ func TestPrinterPrintTableEmptyHeaders(t *testing.T) { assert.Zero(t, buf.Len()) } + +func listData() *stubFormattable { + return &stubFormattable{ + headers: []string{"NAME"}, + rows: [][]string{{"alpha"}, {"beta"}}, + json: map[string]string{"name": "alpha"}, + } +} + +func TestPrinterPrintListShowsHintOnTable(t *testing.T) { + var out, errOut bytes.Buffer + p := &Printer{Writer: &out, ErrWriter: &errOut, Format: FormatTable} + + err := p.PrintList(listData(), &PageInfo{ + Noun: "records", Shown: 30, CurrentPage: 1, TotalPages: 5, TotalEntries: 142, CanFetchAll: true, + }) + if !assert.NoError(t, err) { + return + } + + assert.Contains(t, errOut.String(), "Showing 30 of 142 records (page 1 of 5)") + assert.Contains(t, errOut.String(), "--all") + assert.Contains(t, out.String(), "alpha") +} + +func TestPrinterPrintListHintOmitsAllFlagWhenUnsupported(t *testing.T) { + var out, errOut bytes.Buffer + p := &Printer{Writer: &out, ErrWriter: &errOut, Format: FormatTable} + + err := p.PrintList(listData(), &PageInfo{ + Noun: "certificates", Shown: 30, CurrentPage: 1, TotalPages: 5, TotalEntries: 142, CanFetchAll: false, + }) + if !assert.NoError(t, err) { + return + } + + assert.Contains(t, errOut.String(), "Showing 30 of 142 certificates (page 1 of 5)") + assert.NotContains(t, errOut.String(), "--all") + assert.Contains(t, errOut.String(), "--page") +} + +func TestPrinterPrintListNoHintOnSinglePage(t *testing.T) { + var out, errOut bytes.Buffer + p := &Printer{Writer: &out, ErrWriter: &errOut, Format: FormatTable} + + err := p.PrintList(listData(), &PageInfo{ + Noun: "records", Shown: 2, CurrentPage: 1, TotalPages: 1, TotalEntries: 2, + }) + if !assert.NoError(t, err) { + return + } + + assert.Zero(t, errOut.Len()) + assert.Contains(t, out.String(), "alpha") +} + +func TestPrinterPrintListNoHintWhenInfoNil(t *testing.T) { + var out, errOut bytes.Buffer + p := &Printer{Writer: &out, ErrWriter: &errOut, Format: FormatTable} + + err := p.PrintList(listData(), nil) + if !assert.NoError(t, err) { + return + } + + assert.Zero(t, errOut.Len()) +} + +func TestPrinterPrintListNoHintForJSON(t *testing.T) { + var out, errOut bytes.Buffer + p := &Printer{Writer: &out, ErrWriter: &errOut, Format: FormatJSON} + + err := p.PrintList(listData(), &PageInfo{ + Noun: "records", Shown: 30, CurrentPage: 1, TotalPages: 5, TotalEntries: 142, CanFetchAll: true, + }) + if !assert.NoError(t, err) { + return + } + + assert.Zero(t, errOut.Len()) + assert.Contains(t, out.String(), "alpha") +} From cdf8ab55c22a242316c7f352aa32a761fd86fd5a Mon Sep 17 00:00:00 2001 From: Simone Carletti Date: Mon, 25 May 2026 11:41:07 +0200 Subject: [PATCH 2/4] Refine pagination hint layout and extend paging flags to all list commands --- internal/cli/domains_ds_records.go | 38 ++++++++++++- internal/cli/domains_email_forwards.go | 38 ++++++++++++- internal/cli/domains_pushes.go | 38 ++++++++++++- internal/cli/list_output.go | 1 + internal/cli/output_test.go | 26 +++++++++ internal/cli/registrar_registrant.go | 28 ++++++++++ internal/cli/services.go | 75 ++++++++++++++++++++++++-- internal/cli/templates.go | 38 ++++++++++++- internal/cli/templates_records.go | 38 ++++++++++++- internal/output/format.go | 35 ++++++++---- internal/output/format_test.go | 20 ++++++- 11 files changed, 351 insertions(+), 24 deletions(-) diff --git a/internal/cli/domains_ds_records.go b/internal/cli/domains_ds_records.go index bcbb116..e022981 100644 --- a/internal/cli/domains_ds_records.go +++ b/internal/cli/domains_ds_records.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/dnsimple/cli/internal/cmdutil" + "github.com/dnsimple/cli/internal/pagination" "github.com/dnsimple/dnsimple-go/v8/dnsimple" "github.com/spf13/cobra" ) @@ -80,7 +81,10 @@ func newDomainsDsRecordsCmd(f *cmdutil.Factory) *cobra.Command { } func newDsRecordsListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + var page, perPage int + var all bool + + cmd := &cobra.Command{ Use: "list ", Short: "List DS records", Args: cobra.ExactArgs(1), @@ -95,7 +99,31 @@ func newDsRecordsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Domains.ListDelegationSignerRecords(context.Background(), accountID, args[0], nil) + opts := &dnsimple.ListOptions{} + + if all { + items, err := pagination.All(func(p int) ([]dnsimple.DelegationSignerRecord, *dnsimple.Pagination, error) { + opts.Page = &p + resp, err := c.Domains.ListDelegationSignerRecords(context.Background(), accountID, args[0], opts) + if err != nil { + return nil, nil, err + } + return resp.Data, resp.Pagination, nil + }) + if err != nil { + return err + } + return f.Printer(cmd).Print(&dsRecordList{Data: items}) + } + + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } + + resp, err := c.Domains.ListDelegationSignerRecords(context.Background(), accountID, args[0], opts) if err != nil { return err } @@ -103,6 +131,12 @@ func newDsRecordsListCmd(f *cmdutil.Factory) *cobra.Command { return f.Printer(cmd).PrintList(&dsRecordList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "DS records")) }, } + + cmd.Flags().BoolVar(&all, "all", false, "Fetch all pages") + cmd.Flags().IntVar(&page, "page", 0, "Page number") + cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") + + return cmd } func newDsRecordsGetCmd(f *cmdutil.Factory) *cobra.Command { diff --git a/internal/cli/domains_email_forwards.go b/internal/cli/domains_email_forwards.go index 24ad000..5c71ba0 100644 --- a/internal/cli/domains_email_forwards.go +++ b/internal/cli/domains_email_forwards.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/dnsimple/cli/internal/cmdutil" + "github.com/dnsimple/cli/internal/pagination" "github.com/dnsimple/dnsimple-go/v8/dnsimple" "github.com/spf13/cobra" ) @@ -77,7 +78,10 @@ func newDomainsEmailForwardsCmd(f *cmdutil.Factory) *cobra.Command { } func newEmailForwardsListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + var page, perPage int + var all bool + + cmd := &cobra.Command{ Use: "list ", Short: "List email forwards", Args: cobra.ExactArgs(1), @@ -92,7 +96,31 @@ func newEmailForwardsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Domains.ListEmailForwards(context.Background(), accountID, args[0], nil) + opts := &dnsimple.ListOptions{} + + if all { + items, err := pagination.All(func(p int) ([]dnsimple.EmailForward, *dnsimple.Pagination, error) { + opts.Page = &p + resp, err := c.Domains.ListEmailForwards(context.Background(), accountID, args[0], opts) + if err != nil { + return nil, nil, err + } + return resp.Data, resp.Pagination, nil + }) + if err != nil { + return err + } + return f.Printer(cmd).Print(&emailForwardList{Data: items}) + } + + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } + + resp, err := c.Domains.ListEmailForwards(context.Background(), accountID, args[0], opts) if err != nil { return err } @@ -100,6 +128,12 @@ func newEmailForwardsListCmd(f *cmdutil.Factory) *cobra.Command { return f.Printer(cmd).PrintList(&emailForwardList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "email forwards")) }, } + + cmd.Flags().BoolVar(&all, "all", false, "Fetch all pages") + cmd.Flags().IntVar(&page, "page", 0, "Page number") + cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") + + return cmd } func newEmailForwardsGetCmd(f *cmdutil.Factory) *cobra.Command { diff --git a/internal/cli/domains_pushes.go b/internal/cli/domains_pushes.go index bae88d4..1acb559 100644 --- a/internal/cli/domains_pushes.go +++ b/internal/cli/domains_pushes.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/dnsimple/cli/internal/cmdutil" + "github.com/dnsimple/cli/internal/pagination" "github.com/dnsimple/dnsimple-go/v8/dnsimple" "github.com/spf13/cobra" ) @@ -77,7 +78,10 @@ func newDomainsPushesCmd(f *cmdutil.Factory) *cobra.Command { } func newPushesListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + var page, perPage int + var all bool + + cmd := &cobra.Command{ Use: "list", Short: "List pending domain pushes", RunE: func(cmd *cobra.Command, args []string) error { @@ -91,7 +95,31 @@ func newPushesListCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Domains.ListPushes(context.Background(), accountID, nil) + opts := &dnsimple.ListOptions{} + + if all { + items, err := pagination.All(func(p int) ([]dnsimple.DomainPush, *dnsimple.Pagination, error) { + opts.Page = &p + resp, err := c.Domains.ListPushes(context.Background(), accountID, opts) + if err != nil { + return nil, nil, err + } + return resp.Data, resp.Pagination, nil + }) + if err != nil { + return err + } + return f.Printer(cmd).Print(&pushList{Data: items}) + } + + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } + + resp, err := c.Domains.ListPushes(context.Background(), accountID, opts) if err != nil { return err } @@ -99,6 +127,12 @@ func newPushesListCmd(f *cmdutil.Factory) *cobra.Command { return f.Printer(cmd).PrintList(&pushList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "domain pushes")) }, } + + cmd.Flags().BoolVar(&all, "all", false, "Fetch all pages") + cmd.Flags().IntVar(&page, "page", 0, "Page number") + cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") + + return cmd } func newPushesInitiateCmd(f *cmdutil.Factory) *cobra.Command { diff --git a/internal/cli/list_output.go b/internal/cli/list_output.go index 4ac2632..b96383d 100644 --- a/internal/cli/list_output.go +++ b/internal/cli/list_output.go @@ -20,5 +20,6 @@ func pageHint(cmd *cobra.Command, pg *dnsimple.Pagination, shown int, noun strin TotalPages: pg.TotalPages, TotalEntries: pg.TotalEntries, CanFetchAll: cmd.Flags().Lookup("all") != nil, + CanPaginate: cmd.Flags().Lookup("page") != nil, } } diff --git a/internal/cli/output_test.go b/internal/cli/output_test.go index 533f12a..0efb0d0 100644 --- a/internal/cli/output_test.go +++ b/internal/cli/output_test.go @@ -11,6 +11,7 @@ import ( "github.com/dnsimple/cli/internal/cmdutil" "github.com/dnsimple/cli/internal/config" "github.com/dnsimple/dnsimple-go/v8/dnsimple" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) @@ -262,6 +263,31 @@ func TestRecordsListNoHintOnSinglePage(t *testing.T) { assert.Contains(t, stdout.String(), "1.2.3.4") } +// A paginated list command must expose the navigation flags its hint advertises, +// otherwise the hint points users at flags that error with "unknown flag". +func TestPaginatedListCommandsExposeNavigationFlags(t *testing.T) { + f := cmdutil.NewFactory("test") + commands := map[string]*cobra.Command{ + "services list": newServicesListCmd(f), + "services applied": newServicesAppliedCmd(f), + "templates list": newTemplatesListCmd(f), + "template records list": newTemplateRecordsListCmd(f), + "email-forwards list": newEmailForwardsListCmd(f), + "ds-records list": newDsRecordsListCmd(f), + "pushes list": newPushesListCmd(f), + "registrant-change list": newRegistrantChangeListCmd(f), + "records list": newRecordsListCmd(f), + "domains list": newDomainsListCmd(f), + "zones list": newZonesListCmd(f), + "contacts list": newContactsListCmd(f), + } + for name, cmd := range commands { + for _, flag := range []string{"all", "page", "per-page"} { + assert.NotNilf(t, cmd.Flags().Lookup(flag), "%s should define --%s", name, flag) + } + } +} + func testCLIClient(t *testing.T, handler http.HandlerFunc) (*dnsimple.Client, *config.Config) { t.Helper() diff --git a/internal/cli/registrar_registrant.go b/internal/cli/registrar_registrant.go index bb82244..33c630e 100644 --- a/internal/cli/registrar_registrant.go +++ b/internal/cli/registrar_registrant.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/dnsimple/cli/internal/cmdutil" + "github.com/dnsimple/cli/internal/pagination" "github.com/dnsimple/dnsimple-go/v8/dnsimple" "github.com/spf13/cobra" ) @@ -77,6 +78,8 @@ func newRegistrarRegistrantChangeCmd(f *cmdutil.Factory) *cobra.Command { func newRegistrantChangeListCmd(f *cmdutil.Factory) *cobra.Command { var state, domainID, contactID string + var page, perPage int + var all bool cmd := &cobra.Command{ Use: "list", @@ -102,6 +105,28 @@ func newRegistrantChangeListCmd(f *cmdutil.Factory) *cobra.Command { opts.ContactId = &contactID } + if all { + items, err := pagination.All(func(p int) ([]dnsimple.RegistrantChange, *dnsimple.Pagination, error) { + opts.Page = &p + resp, err := c.Registrar.ListRegistrantChange(context.Background(), accountID, opts) + if err != nil { + return nil, nil, err + } + return resp.Data, resp.Pagination, nil + }) + if err != nil { + return err + } + return f.Printer(cmd).Print(®istrantChangeList{Data: items}) + } + + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } + resp, err := c.Registrar.ListRegistrantChange(context.Background(), accountID, opts) if err != nil { return err @@ -114,6 +139,9 @@ func newRegistrantChangeListCmd(f *cmdutil.Factory) *cobra.Command { cmd.Flags().StringVar(&state, "state", "", "Filter by state") cmd.Flags().StringVar(&domainID, "domain-id", "", "Filter by domain ID") cmd.Flags().StringVar(&contactID, "contact-id", "", "Filter by contact ID") + cmd.Flags().BoolVar(&all, "all", false, "Fetch all pages") + cmd.Flags().IntVar(&page, "page", 0, "Page number") + cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") return cmd } diff --git a/internal/cli/services.go b/internal/cli/services.go index d35a5d6..76179c4 100644 --- a/internal/cli/services.go +++ b/internal/cli/services.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/dnsimple/cli/internal/cmdutil" + "github.com/dnsimple/cli/internal/pagination" "github.com/dnsimple/dnsimple-go/v8/dnsimple" "github.com/spf13/cobra" ) @@ -77,7 +78,10 @@ func newServicesCmd(f *cmdutil.Factory) *cobra.Command { } func newServicesListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + var page, perPage int + var all bool + + cmd := &cobra.Command{ Use: "list", Short: "List available one-click services", RunE: func(cmd *cobra.Command, args []string) error { @@ -86,7 +90,31 @@ func newServicesListCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Services.ListServices(context.Background(), nil) + opts := &dnsimple.ListOptions{} + + if all { + items, err := pagination.All(func(p int) ([]dnsimple.Service, *dnsimple.Pagination, error) { + opts.Page = &p + resp, err := c.Services.ListServices(context.Background(), opts) + if err != nil { + return nil, nil, err + } + return resp.Data, resp.Pagination, nil + }) + if err != nil { + return err + } + return f.Printer(cmd).Print(&serviceList{Data: items}) + } + + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } + + resp, err := c.Services.ListServices(context.Background(), opts) if err != nil { return err } @@ -94,6 +122,12 @@ func newServicesListCmd(f *cmdutil.Factory) *cobra.Command { return f.Printer(cmd).PrintList(&serviceList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "services")) }, } + + cmd.Flags().BoolVar(&all, "all", false, "Fetch all pages") + cmd.Flags().IntVar(&page, "page", 0, "Page number") + cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") + + return cmd } func newServicesGetCmd(f *cmdutil.Factory) *cobra.Command { @@ -118,7 +152,10 @@ func newServicesGetCmd(f *cmdutil.Factory) *cobra.Command { } func newServicesAppliedCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + var page, perPage int + var all bool + + cmd := &cobra.Command{ Use: "applied ", Short: "List services applied to a domain", Args: cobra.ExactArgs(1), @@ -133,7 +170,31 @@ func newServicesAppliedCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Services.AppliedServices(context.Background(), accountID, args[0], nil) + opts := &dnsimple.ListOptions{} + + if all { + items, err := pagination.All(func(p int) ([]dnsimple.Service, *dnsimple.Pagination, error) { + opts.Page = &p + resp, err := c.Services.AppliedServices(context.Background(), accountID, args[0], opts) + if err != nil { + return nil, nil, err + } + return resp.Data, resp.Pagination, nil + }) + if err != nil { + return err + } + return f.Printer(cmd).Print(&serviceList{Data: items}) + } + + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } + + resp, err := c.Services.AppliedServices(context.Background(), accountID, args[0], opts) if err != nil { return err } @@ -141,6 +202,12 @@ func newServicesAppliedCmd(f *cmdutil.Factory) *cobra.Command { return f.Printer(cmd).PrintList(&serviceList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "applied services")) }, } + + cmd.Flags().BoolVar(&all, "all", false, "Fetch all pages") + cmd.Flags().IntVar(&page, "page", 0, "Page number") + cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") + + return cmd } func newServicesApplyCmd(f *cmdutil.Factory) *cobra.Command { diff --git a/internal/cli/templates.go b/internal/cli/templates.go index 02573d0..d4f0517 100644 --- a/internal/cli/templates.go +++ b/internal/cli/templates.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/dnsimple/cli/internal/cmdutil" + "github.com/dnsimple/cli/internal/pagination" "github.com/dnsimple/dnsimple-go/v8/dnsimple" "github.com/spf13/cobra" ) @@ -78,7 +79,10 @@ func newTemplatesCmd(f *cmdutil.Factory) *cobra.Command { } func newTemplatesListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + var page, perPage int + var all bool + + cmd := &cobra.Command{ Use: "list", Short: "List templates", RunE: func(cmd *cobra.Command, args []string) error { @@ -91,7 +95,31 @@ func newTemplatesListCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Templates.ListTemplates(context.Background(), accountID, nil) + opts := &dnsimple.ListOptions{} + + if all { + items, err := pagination.All(func(p int) ([]dnsimple.Template, *dnsimple.Pagination, error) { + opts.Page = &p + resp, err := c.Templates.ListTemplates(context.Background(), accountID, opts) + if err != nil { + return nil, nil, err + } + return resp.Data, resp.Pagination, nil + }) + if err != nil { + return err + } + return f.Printer(cmd).Print(&templateList{Data: items}) + } + + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } + + resp, err := c.Templates.ListTemplates(context.Background(), accountID, opts) if err != nil { return err } @@ -99,6 +127,12 @@ func newTemplatesListCmd(f *cmdutil.Factory) *cobra.Command { return f.Printer(cmd).PrintList(&templateList{Data: resp.Data, Pagination: resp.Pagination}, pageHint(cmd, resp.Pagination, len(resp.Data), "templates")) }, } + + cmd.Flags().BoolVar(&all, "all", false, "Fetch all pages") + cmd.Flags().IntVar(&page, "page", 0, "Page number") + cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") + + return cmd } func newTemplatesGetCmd(f *cmdutil.Factory) *cobra.Command { diff --git a/internal/cli/templates_records.go b/internal/cli/templates_records.go index 8717eae..98644a1 100644 --- a/internal/cli/templates_records.go +++ b/internal/cli/templates_records.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/dnsimple/cli/internal/cmdutil" + "github.com/dnsimple/cli/internal/pagination" "github.com/dnsimple/dnsimple-go/v8/dnsimple" "github.com/spf13/cobra" ) @@ -79,7 +80,10 @@ func newTemplatesRecordsCmd(f *cmdutil.Factory) *cobra.Command { } func newTemplateRecordsListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + var page, perPage int + var all bool + + cmd := &cobra.Command{ Use: "list