diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ac6dc..1cd870e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ This project uses [Semantic Versioning 2.0.0](http://semver.org/), the format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased + +### Added + +- List commands now show a `Showing X of Y` pagination hint and expose `--all`/`--page`/`--per-page` consistently, making it obvious when results span multiple pages and how to retrieve the rest. (dnsimple/cli#51) + ## 0.8.0 - 2026-05-14 ### Changed 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..ed6c4fb 100644 --- a/internal/cli/analytics.go +++ b/internal/cli/analytics.go @@ -113,7 +113,7 @@ func newAnalyticsCmd(f *cmdutil.Factory) *cobra.Command { func newAnalyticsQueryCmd(f *cmdutil.Factory) *cobra.Command { var startDate, endDate, groupings, sort string - var page, perPage int + lf := &listFlags{} cmd := &cobra.Command{ Use: "query", @@ -151,23 +151,24 @@ func newAnalyticsQueryCmd(f *cmdutil.Factory) *cobra.Command { if sort != "" { opts.Sort = &sort } - if page > 0 { - opts.Page = &page - } - if perPage > 0 { - opts.PerPage = &perPage - } - - resp, err := c.DnsAnalytics.Query(context.Background(), accountID, opts) - if err != nil { - return err - } - return f.Printer(cmd).Print(&analyticsOutput{ - Data: resp.Data, - Pagination: resp.Pagination, - Groupings: effectiveGroupings, - }) + return runList(cmd, f, lf, "analytics rows", + func(page, perPage int) ([]dnsimple.DnsAnalytics, *dnsimple.Pagination, error) { + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } + resp, err := c.DnsAnalytics.Query(context.Background(), accountID, opts) + if err != nil { + return nil, nil, err + } + return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.DnsAnalytics, pg *dnsimple.Pagination) *analyticsOutput { + return &analyticsOutput{Data: items, Pagination: pg, Groupings: effectiveGroupings} + }) }, } @@ -175,8 +176,7 @@ func newAnalyticsQueryCmd(f *cmdutil.Factory) *cobra.Command { cmd.Flags().StringVar(&endDate, "end-date", "", "End date (ISO8601)") cmd.Flags().StringVar(&groupings, "groupings", "", "Group by (comma-separated). Supported: zone_name, date") cmd.Flags().StringVar(&sort, "sort", "", "Sort order") - cmd.Flags().IntVar(&page, "page", 0, "Page number") - cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") + lf.register(cmd) return cmd } 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..e243151 100644 --- a/internal/cli/certificates.go +++ b/internal/cli/certificates.go @@ -164,8 +164,8 @@ func newCertificatesCmd(f *cmdutil.Factory) *cobra.Command { } func newCertsListCmd(f *cmdutil.Factory) *cobra.Command { - var page, perPage int var sort string + lf := &listFlags{} cmd := &cobra.Command{ Use: "list ", @@ -183,28 +183,32 @@ func newCertsListCmd(f *cmdutil.Factory) *cobra.Command { } opts := &dnsimple.ListOptions{} - if page > 0 { - opts.Page = &page - } - if perPage > 0 { - opts.PerPage = &perPage - } if sort != "" { opts.Sort = &sort } - resp, err := c.Certificates.ListCertificates(context.Background(), accountID, args[0], opts) - if err != nil { - return err - } - - return f.Printer(cmd).Print(&certList{Data: resp.Data, Pagination: resp.Pagination}) + return runList(cmd, f, lf, "certificates", + func(page, perPage int) ([]dnsimple.Certificate, *dnsimple.Pagination, error) { + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } + resp, err := c.Certificates.ListCertificates(context.Background(), accountID, args[0], opts) + if err != nil { + return nil, nil, err + } + return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.Certificate, pg *dnsimple.Pagination) *certList { + return &certList{Data: items, Pagination: pg} + }) }, } cmd.Flags().StringVar(&sort, "sort", "", "Sort order") - cmd.Flags().IntVar(&page, "page", 0, "Page number") - cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") + lf.register(cmd) return cmd } diff --git a/internal/cli/contacts.go b/internal/cli/contacts.go index b7f779b..6f2faac 100644 --- a/internal/cli/contacts.go +++ b/internal/cli/contacts.go @@ -6,7 +6,6 @@ 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" ) @@ -94,9 +93,8 @@ func newContactsCmd(f *cmdutil.Factory) *cobra.Command { } func newContactsListCmd(f *cmdutil.Factory) *cobra.Command { - var page, perPage int var sort string - var all bool + lf := &listFlags{} cmd := &cobra.Command{ Use: "list", @@ -117,41 +115,28 @@ func newContactsListCmd(f *cmdutil.Factory) *cobra.Command { opts.Sort = &sort } - if all { - items, err := pagination.All(func(p int) ([]dnsimple.Contact, *dnsimple.Pagination, error) { - opts.Page = &p + return runList(cmd, f, lf, "contacts", + func(page, perPage int) ([]dnsimple.Contact, *dnsimple.Pagination, error) { + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } resp, err := c.Contacts.ListContacts(context.Background(), accountID, opts) if err != nil { return nil, nil, err } return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.Contact, pg *dnsimple.Pagination) *contactList { + return &contactList{Data: items, Pagination: pg} }) - if err != nil { - return err - } - return f.Printer(cmd).Print(&contactList{Data: items}) - } - - if page > 0 { - opts.Page = &page - } - if perPage > 0 { - opts.PerPage = &perPage - } - - resp, err := c.Contacts.ListContacts(context.Background(), accountID, opts) - if err != nil { - return err - } - - return f.Printer(cmd).Print(&contactList{Data: resp.Data, Pagination: resp.Pagination}) }, } - cmd.Flags().BoolVar(&all, "all", false, "Fetch all pages") cmd.Flags().StringVar(&sort, "sort", "", "Sort order") - cmd.Flags().IntVar(&page, "page", 0, "Page number") - cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") + lf.register(cmd) return cmd } diff --git a/internal/cli/domains.go b/internal/cli/domains.go index b398b62..c6aa0a9 100644 --- a/internal/cli/domains.go +++ b/internal/cli/domains.go @@ -8,7 +8,6 @@ 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" ) @@ -97,10 +96,8 @@ func newDomainsCmd(f *cmdutil.Factory) *cobra.Command { } func newDomainsListCmd(f *cmdutil.Factory) *cobra.Command { - var nameLike string - var page, perPage int - var sort string - var all bool + var nameLike, sort string + lf := &listFlags{} cmd := &cobra.Command{ Use: "list", @@ -124,42 +121,29 @@ func newDomainsListCmd(f *cmdutil.Factory) *cobra.Command { opts.Sort = &sort } - if all { - items, err := pagination.All(func(p int) ([]dnsimple.Domain, *dnsimple.Pagination, error) { - opts.Page = &p + return runList(cmd, f, lf, "domains", + func(page, perPage int) ([]dnsimple.Domain, *dnsimple.Pagination, error) { + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } resp, err := c.Domains.ListDomains(context.Background(), accountID, opts) if err != nil { return nil, nil, err } return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.Domain, pg *dnsimple.Pagination) *domainList { + return &domainList{Data: items, Pagination: pg} }) - if err != nil { - return err - } - return f.Printer(cmd).Print(&domainList{Data: items}) - } - - if page > 0 { - opts.Page = &page - } - if perPage > 0 { - opts.PerPage = &perPage - } - - resp, err := c.Domains.ListDomains(context.Background(), accountID, opts) - if err != nil { - return err - } - - return f.Printer(cmd).Print(&domainList{Data: resp.Data, Pagination: resp.Pagination}) }, } cmd.Flags().StringVar(&nameLike, "name-like", "", "Filter domains by name (partial match)") - cmd.Flags().BoolVar(&all, "all", false, "Fetch all pages") cmd.Flags().StringVar(&sort, "sort", "", "Sort order (e.g., name:asc, expiration:desc)") - cmd.Flags().IntVar(&page, "page", 0, "Page number") - cmd.Flags().IntVar(&perPage, "per-page", 0, "Number of items per page") + lf.register(cmd) return cmd } diff --git a/internal/cli/domains_ds_records.go b/internal/cli/domains_ds_records.go index dee6976..157c37f 100644 --- a/internal/cli/domains_ds_records.go +++ b/internal/cli/domains_ds_records.go @@ -80,7 +80,9 @@ func newDomainsDsRecordsCmd(f *cmdutil.Factory) *cobra.Command { } func newDsRecordsListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + lf := &listFlags{} + + cmd := &cobra.Command{ Use: "list ", Short: "List DS records", Args: cobra.ExactArgs(1), @@ -95,14 +97,31 @@ func newDsRecordsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Domains.ListDelegationSignerRecords(context.Background(), accountID, args[0], nil) - if err != nil { - return err - } - - return f.Printer(cmd).Print(&dsRecordList{Data: resp.Data, Pagination: resp.Pagination}) + opts := &dnsimple.ListOptions{} + + return runList(cmd, f, lf, "DS records", + func(page, perPage int) ([]dnsimple.DelegationSignerRecord, *dnsimple.Pagination, error) { + 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 nil, nil, err + } + return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.DelegationSignerRecord, pg *dnsimple.Pagination) *dsRecordList { + return &dsRecordList{Data: items, Pagination: pg} + }) }, } + + lf.register(cmd) + + 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 3cac9b0..648d39a 100644 --- a/internal/cli/domains_email_forwards.go +++ b/internal/cli/domains_email_forwards.go @@ -77,7 +77,9 @@ func newDomainsEmailForwardsCmd(f *cmdutil.Factory) *cobra.Command { } func newEmailForwardsListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + lf := &listFlags{} + + cmd := &cobra.Command{ Use: "list ", Short: "List email forwards", Args: cobra.ExactArgs(1), @@ -92,14 +94,31 @@ func newEmailForwardsListCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Domains.ListEmailForwards(context.Background(), accountID, args[0], nil) - if err != nil { - return err - } - - return f.Printer(cmd).Print(&emailForwardList{Data: resp.Data, Pagination: resp.Pagination}) + opts := &dnsimple.ListOptions{} + + return runList(cmd, f, lf, "email forwards", + func(page, perPage int) ([]dnsimple.EmailForward, *dnsimple.Pagination, error) { + 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 nil, nil, err + } + return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.EmailForward, pg *dnsimple.Pagination) *emailForwardList { + return &emailForwardList{Data: items, Pagination: pg} + }) }, } + + lf.register(cmd) + + return cmd } func newEmailForwardsGetCmd(f *cmdutil.Factory) *cobra.Command { diff --git a/internal/cli/domains_pushes.go b/internal/cli/domains_pushes.go index 472cad6..df48338 100644 --- a/internal/cli/domains_pushes.go +++ b/internal/cli/domains_pushes.go @@ -77,7 +77,9 @@ func newDomainsPushesCmd(f *cmdutil.Factory) *cobra.Command { } func newPushesListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + lf := &listFlags{} + + cmd := &cobra.Command{ Use: "list", Short: "List pending domain pushes", RunE: func(cmd *cobra.Command, args []string) error { @@ -91,14 +93,31 @@ func newPushesListCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Domains.ListPushes(context.Background(), accountID, nil) - if err != nil { - return err - } - - return f.Printer(cmd).Print(&pushList{Data: resp.Data, Pagination: resp.Pagination}) + opts := &dnsimple.ListOptions{} + + return runList(cmd, f, lf, "domain pushes", + func(page, perPage int) ([]dnsimple.DomainPush, *dnsimple.Pagination, error) { + 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 nil, nil, err + } + return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.DomainPush, pg *dnsimple.Pagination) *pushList { + return &pushList{Data: items, Pagination: pg} + }) }, } + + lf.register(cmd) + + return cmd } func newPushesInitiateCmd(f *cmdutil.Factory) *cobra.Command { diff --git a/internal/cli/list_output.go b/internal/cli/list_output.go new file mode 100644 index 0000000..9bf3a45 --- /dev/null +++ b/internal/cli/list_output.go @@ -0,0 +1,71 @@ +package cli + +import ( + "github.com/dnsimple/cli/internal/cmdutil" + "github.com/dnsimple/cli/internal/output" + "github.com/dnsimple/cli/internal/pagination" + "github.com/dnsimple/dnsimple-go/v8/dnsimple" + "github.com/spf13/cobra" +) + +// listFlags holds the pagination flags shared by every list command. +type listFlags struct { + page int + perPage int + all bool +} + +// register adds the standard --all/--page/--per-page flags to cmd. +func (lf *listFlags) register(cmd *cobra.Command) { + cmd.Flags().BoolVar(&lf.all, "all", false, "Fetch all pages") + cmd.Flags().IntVar(&lf.page, "page", 0, "Page number") + cmd.Flags().IntVar(&lf.perPage, "per-page", 0, "Number of items per page") +} + +// runList executes a paginated list command. fetch retrieves a single page for +// the given page/per-page (0 means "use the API default"); wrap adapts a page of +// results into a renderable value. With --all every page is fetched and the hint +// is skipped; otherwise the requested page is rendered with a discovery hint for +// table output. +func runList[T any, F output.Formattable]( + cmd *cobra.Command, + f *cmdutil.Factory, + lf *listFlags, + noun string, + fetch func(page, perPage int) ([]T, *dnsimple.Pagination, error), + wrap func(items []T, pg *dnsimple.Pagination) F, +) error { + if lf.all { + items, err := pagination.All(func(p int) ([]T, *dnsimple.Pagination, error) { + return fetch(p, lf.perPage) + }) + if err != nil { + return err + } + return f.Printer(cmd).Print(wrap(items, nil)) + } + + items, pg, err := fetch(lf.page, lf.perPage) + if err != nil { + return err + } + return f.Printer(cmd).PrintList(wrap(items, pg), pageHint(cmd, pg, len(items), noun)) +} + +// 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, + CanPaginate: cmd.Flags().Lookup("page") != nil, + } +} diff --git a/internal/cli/output_test.go b/internal/cli/output_test.go index 08d1393..6243b39 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" ) @@ -183,6 +184,113 @@ 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") +} + +// 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), + "certificates list": newCertsListCmd(f), + "tlds list": newTldsListCmd(f), + "analytics query": newAnalyticsQueryCmd(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 441b145..1bf400e 100644 --- a/internal/cli/registrar_registrant.go +++ b/internal/cli/registrar_registrant.go @@ -77,6 +77,7 @@ func newRegistrarRegistrantChangeCmd(f *cmdutil.Factory) *cobra.Command { func newRegistrantChangeListCmd(f *cmdutil.Factory) *cobra.Command { var state, domainID, contactID string + lf := &listFlags{} cmd := &cobra.Command{ Use: "list", @@ -102,18 +103,30 @@ func newRegistrantChangeListCmd(f *cmdutil.Factory) *cobra.Command { opts.ContactId = &contactID } - resp, err := c.Registrar.ListRegistrantChange(context.Background(), accountID, opts) - if err != nil { - return err - } - - return f.Printer(cmd).Print(®istrantChangeList{Data: resp.Data, Pagination: resp.Pagination}) + return runList(cmd, f, lf, "registrant changes", + func(page, perPage int) ([]dnsimple.RegistrantChange, *dnsimple.Pagination, error) { + 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 nil, nil, err + } + return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.RegistrantChange, pg *dnsimple.Pagination) *registrantChangeList { + return ®istrantChangeList{Data: items, Pagination: pg} + }) }, } 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") + lf.register(cmd) return cmd } diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 11a8aff..730091f 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -20,12 +20,15 @@ func TestExecuteUnknownCommandReturnsError(t *testing.T) { } func TestHelpUsesDeclaredLocalFlagOrder(t *testing.T) { + // Command-specific flags are declared first; the shared pagination flags + // (--all/--page/--per-page) are registered together at the end. analyticsHelp := commandHelpOutput(t, "analytics", "query") assertContainsInOrder(t, analyticsHelp, "--start-date", "--end-date", "--groupings", "--sort", + "--all", "--page", "--per-page", ) @@ -33,8 +36,8 @@ func TestHelpUsesDeclaredLocalFlagOrder(t *testing.T) { domainsHelp := commandHelpOutput(t, "domains", "list") assertContainsInOrder(t, domainsHelp, "--name-like", - "--all", "--sort", + "--all", "--page", "--per-page", ) diff --git a/internal/cli/services.go b/internal/cli/services.go index ed386c8..287583a 100644 --- a/internal/cli/services.go +++ b/internal/cli/services.go @@ -77,7 +77,9 @@ func newServicesCmd(f *cmdutil.Factory) *cobra.Command { } func newServicesListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + lf := &listFlags{} + + cmd := &cobra.Command{ Use: "list", Short: "List available one-click services", RunE: func(cmd *cobra.Command, args []string) error { @@ -86,14 +88,31 @@ func newServicesListCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Services.ListServices(context.Background(), nil) - if err != nil { - return err - } - - return f.Printer(cmd).Print(&serviceList{Data: resp.Data, Pagination: resp.Pagination}) + opts := &dnsimple.ListOptions{} + + return runList(cmd, f, lf, "services", + func(page, perPage int) ([]dnsimple.Service, *dnsimple.Pagination, error) { + if page > 0 { + opts.Page = &page + } + if perPage > 0 { + opts.PerPage = &perPage + } + resp, err := c.Services.ListServices(context.Background(), opts) + if err != nil { + return nil, nil, err + } + return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.Service, pg *dnsimple.Pagination) *serviceList { + return &serviceList{Data: items, Pagination: pg} + }) }, } + + lf.register(cmd) + + return cmd } func newServicesGetCmd(f *cmdutil.Factory) *cobra.Command { @@ -118,7 +137,9 @@ func newServicesGetCmd(f *cmdutil.Factory) *cobra.Command { } func newServicesAppliedCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + lf := &listFlags{} + + cmd := &cobra.Command{ Use: "applied ", Short: "List services applied to a domain", Args: cobra.ExactArgs(1), @@ -133,14 +154,31 @@ func newServicesAppliedCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Services.AppliedServices(context.Background(), accountID, args[0], nil) - if err != nil { - return err - } - - return f.Printer(cmd).Print(&serviceList{Data: resp.Data, Pagination: resp.Pagination}) + opts := &dnsimple.ListOptions{} + + return runList(cmd, f, lf, "applied services", + func(page, perPage int) ([]dnsimple.Service, *dnsimple.Pagination, error) { + 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 nil, nil, err + } + return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.Service, pg *dnsimple.Pagination) *serviceList { + return &serviceList{Data: items, Pagination: pg} + }) }, } + + lf.register(cmd) + + return cmd } func newServicesApplyCmd(f *cmdutil.Factory) *cobra.Command { diff --git a/internal/cli/templates.go b/internal/cli/templates.go index 0c280ce..ee594b1 100644 --- a/internal/cli/templates.go +++ b/internal/cli/templates.go @@ -78,7 +78,9 @@ func newTemplatesCmd(f *cmdutil.Factory) *cobra.Command { } func newTemplatesListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + lf := &listFlags{} + + cmd := &cobra.Command{ Use: "list", Short: "List templates", RunE: func(cmd *cobra.Command, args []string) error { @@ -91,14 +93,31 @@ func newTemplatesListCmd(f *cmdutil.Factory) *cobra.Command { return err } - resp, err := c.Templates.ListTemplates(context.Background(), accountID, nil) - if err != nil { - return err - } - - return f.Printer(cmd).Print(&templateList{Data: resp.Data, Pagination: resp.Pagination}) + opts := &dnsimple.ListOptions{} + + return runList(cmd, f, lf, "templates", + func(page, perPage int) ([]dnsimple.Template, *dnsimple.Pagination, error) { + 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 nil, nil, err + } + return resp.Data, resp.Pagination, nil + }, + func(items []dnsimple.Template, pg *dnsimple.Pagination) *templateList { + return &templateList{Data: items, Pagination: pg} + }) }, } + + lf.register(cmd) + + return cmd } func newTemplatesGetCmd(f *cmdutil.Factory) *cobra.Command { diff --git a/internal/cli/templates_records.go b/internal/cli/templates_records.go index a95823a..8ed3244 100644 --- a/internal/cli/templates_records.go +++ b/internal/cli/templates_records.go @@ -79,7 +79,9 @@ func newTemplatesRecordsCmd(f *cmdutil.Factory) *cobra.Command { } func newTemplateRecordsListCmd(f *cmdutil.Factory) *cobra.Command { - return &cobra.Command{ + lf := &listFlags{} + + cmd := &cobra.Command{ Use: "list