Skip to content
Merged
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
187 changes: 187 additions & 0 deletions internal/cli/records.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package cli

import (
"fmt"
"os"
"strconv"

"github.com/ran-codes/zenodo-cli/internal/api"
"github.com/ran-codes/zenodo-cli/internal/model"
"github.com/ran-codes/zenodo-cli/internal/output"
"github.com/spf13/cobra"
)

var recordsCmd = &cobra.Command{
Use: "records",
Short: "List, search, and view records",
}

var recordsListCmd = &cobra.Command{
Use: "list",
Short: "List your records and drafts",
Long: `List the authenticated user's records and drafts.

Examples:
zenodo records list
zenodo records list --status draft
zenodo records list --community my-org --all`,
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(appCtx.BaseURL, appCtx.Token)
status, _ := cmd.Flags().GetString("status")
community, _ := cmd.Flags().GetString("community")
all, _ := cmd.Flags().GetBool("all")

params := api.RecordListParams{
Status: status,
Community: community,
}

if all {
records, total, err := api.PaginateAll(func(page int) (*model.RecordSearchResult, error) {
params.Page = page
return client.ListUserRecords(params)
})
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
}
fmt.Fprintf(os.Stderr, "Total: %d records\n", total)
return output.Format(os.Stdout, records, appCtx.Output, appCtx.Fields)
}

result, err := client.ListUserRecords(params)
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Showing %d of %d records\n", len(result.Hits.Hits), result.Hits.Total)
return output.Format(os.Stdout, result.Hits.Hits, appCtx.Output, appCtx.Fields)
},
}

var recordsSearchCmd = &cobra.Command{
Use: "search <query>",
Short: "Search published records",
Long: `Search all published records using Elasticsearch query syntax.

Examples:
zenodo records search "climate change"
zenodo records search --community my-org "dataset"
zenodo records search "publication_date:[2024-01-01 TO 2024-12-31]" --all`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(appCtx.BaseURL, appCtx.Token)
query := args[0]
community, _ := cmd.Flags().GetString("community")
all, _ := cmd.Flags().GetBool("all")

params := api.RecordListParams{
Community: community,
}

if all {
records, total, err := api.PaginateAll(func(page int) (*model.RecordSearchResult, error) {
params.Page = page
return client.SearchRecords(query, params)
})
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
}
fmt.Fprintf(os.Stderr, "Total: %d records\n", total)
return output.Format(os.Stdout, records, appCtx.Output, appCtx.Fields)
}

result, err := client.SearchRecords(query, params)
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Showing %d of %d records\n", len(result.Hits.Hits), result.Hits.Total)
return output.Format(os.Stdout, result.Hits.Hits, appCtx.Output, appCtx.Fields)
},
}

var recordsGetCmd = &cobra.Command{
Use: "get <id>",
Short: "Get a record by ID",
Long: `Retrieve full details of a published record.

Examples:
zenodo records get 12345
zenodo records get 12345 --output json
zenodo records get 12345 --format bibtex`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid record ID: %s", args[0])
}

client := api.NewClient(appCtx.BaseURL, appCtx.Token)
format, _ := cmd.Flags().GetString("format")

// Handle non-JSON formats via Accept header.
switch format {
case "bibtex":
data, err := client.GetRaw(fmt.Sprintf("/records/%d", id), "application/x-bibtex")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
case "datacite":
data, err := client.GetRaw(fmt.Sprintf("/records/%d", id), "application/vnd.datacite.datacite+xml")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}
Comment on lines +121 to +136
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format flag handling has an inconsistency. When format is "json", it falls through to the default case and uses the output formatter with appCtx.Output, instead of using the Accept header approach like bibtex and datacite. This means --format json and --output json would behave differently under the hood. Consider adding a case for "json" that uses GetRaw with "application/json" Accept header for consistency, or explicitly document that --format json is equivalent to --output json.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +136
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format flag validation is missing. If a user provides an invalid format value (e.g., --format pdf), it silently falls through to the default behavior instead of returning an error. Consider adding validation to ensure format is one of the documented values ("bibtex", "datacite", or empty for default). Valid format values could be checked before the switch statement, or a default case could be added to return an error like: fmt.Errorf("unsupported format %q; supported: json, bibtex, datacite", format)

Copilot uses AI. Check for mistakes.

record, err := client.GetRecord(id)
if err != nil {
return err
}
return output.Format(os.Stdout, record, appCtx.Output, appCtx.Fields)
},
}

var recordsVersionsCmd = &cobra.Command{
Use: "versions <id>",
Short: "List all versions of a record",
Long: `List all versions of a record by its ID.

Examples:
zenodo records versions 12345`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid record ID: %s", args[0])
}

client := api.NewClient(appCtx.BaseURL, appCtx.Token)
result, err := client.ListVersions(id)
if err != nil {
return err
}
return output.Format(os.Stdout, result.Hits.Hits, appCtx.Output, appCtx.Fields)
},
}

func init() {
// records list flags
recordsListCmd.Flags().String("status", "", "Filter by status: draft, published")
recordsListCmd.Flags().String("community", "", "Filter by community ID")
recordsListCmd.Flags().Bool("all", false, "Fetch all pages (up to 10k results)")

// records search flags
recordsSearchCmd.Flags().String("community", "", "Filter by community ID")
recordsSearchCmd.Flags().Bool("all", false, "Fetch all pages (up to 10k results)")

// records get flags
recordsGetCmd.Flags().String("format", "", "Response format: json, bibtex, datacite (default: uses --output)")
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format flag help text lists "json, bibtex, datacite" but doesn't mention "table" which is specified in requirement FR-2.3. Additionally, the help text should clarify the behavior: when format is empty, it uses the --output flag setting; when format is "bibtex" or "datacite", it requests that specific format from the API via Accept headers. The current text "(default: uses --output)" is somewhat ambiguous.

Suggested change
recordsGetCmd.Flags().String("format", "", "Response format: json, bibtex, datacite (default: uses --output)")
recordsGetCmd.Flags().String("format", "", "Response format: table, json, bibtex, datacite. When empty, uses --output; bibtex/datacite are requested via Accept headers.")

Copilot uses AI. Check for mistakes.

recordsCmd.AddCommand(recordsListCmd)
recordsCmd.AddCommand(recordsSearchCmd)
recordsCmd.AddCommand(recordsGetCmd)
recordsCmd.AddCommand(recordsVersionsCmd)
rootCmd.AddCommand(recordsCmd)
}
Comment on lines +1 to +187
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new records CLI commands lack test coverage. While the underlying API layer (internal/api/records.go) and output formatters (internal/output/) have comprehensive tests, the CLI commands themselves are untested. Consider adding tests that verify:

  1. Flag parsing and validation (e.g., status, community, all, format)
  2. Error handling when API calls fail
  3. Correct delegation to API client methods
  4. Output formatting via the output package
  5. stderr vs stdout output separation

This would align with the testing patterns established in other packages like internal/api, internal/config, and internal/output which all have comprehensive test files.

Copilot uses AI. Check for mistakes.