From f9e838393f972c7b7d8dde5387c929aac8dbe3f2 Mon Sep 17 00:00:00 2001 From: Duc-Tam Nguyen Date: Tue, 16 Jun 2026 01:13:56 +0700 Subject: [PATCH 1/2] add --skip flag to search/recent and SearchFrom/RecentFrom client methods Lets callers start from an arbitrary offset -- useful when paging through results or resuming from a known position. Rank is adjusted so records start from skip+1. Also adds two new tests: TestSearchFromSkip and TestRateLimitedOn429. --- chemrxiv/chemrxiv.go | 21 +++++++++++++---- chemrxiv/chemrxiv_test.go | 48 +++++++++++++++++++++++++++++++++++++++ cli/cmd_recent.go | 9 +++++--- cli/cmd_search.go | 9 +++++--- 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/chemrxiv/chemrxiv.go b/chemrxiv/chemrxiv.go index 8bac04b..983863d 100644 --- a/chemrxiv/chemrxiv.go +++ b/chemrxiv/chemrxiv.go @@ -85,6 +85,12 @@ func (c *Client) graphqlEndpoint() string { // Search fetches preprints matching term. An empty term returns the most // recent preprints. Returns up to limit results and the server total count. func (c *Client) Search(ctx context.Context, term string, limit int) ([]Preprint, int, error) { + return c.SearchFrom(ctx, term, 0, limit) +} + +// SearchFrom fetches preprints matching term starting from the given skip +// offset. Returns up to limit results and the server total count. +func (c *Client) SearchFrom(ctx context.Context, term string, skip, limit int) ([]Preprint, int, error) { if limit <= 0 { limit = 20 } @@ -92,7 +98,7 @@ func (c *Client) Search(ctx context.Context, term string, limit int) ([]Preprint const pageSize = 25 var out []Preprint total := 0 - skip := 0 + cursor := skip for { fetch := pageSize @@ -102,7 +108,7 @@ func (c *Client) Search(ctx context.Context, term string, limit int) ([]Preprint q := fmt.Sprintf( `{ itemsByKeyword(skip:%d, limit:%d, term:%s) { totalCount itemHits { item { id doi title statusDate publishedDate version authors { firstName lastName institutions { name } } categories { name } keywords license metrics { viewCount downloadCount } asset { original { url } } } } } }`, - skip, fetch, jsonString(term), + cursor, fetch, jsonString(term), ) var resp graphqlResponse[searchData] @@ -117,7 +123,7 @@ func (c *Client) Search(ctx context.Context, term string, limit int) ([]Preprint total = result.TotalCount for _, hit := range result.ItemHits { - out = append(out, wireToPreprint(hit.Item, len(out)+1)) + out = append(out, wireToPreprint(hit.Item, skip+len(out)+1)) if len(out) >= limit { return out, total, nil } @@ -126,14 +132,19 @@ func (c *Client) Search(ctx context.Context, term string, limit int) ([]Preprint if len(result.ItemHits) < fetch { break } - skip += fetch + cursor += fetch } return out, total, nil } // Recent returns the most recent preprints (empty term search). func (c *Client) Recent(ctx context.Context, limit int) ([]Preprint, int, error) { - return c.Search(ctx, "", limit) + return c.SearchFrom(ctx, "", 0, limit) +} + +// RecentFrom returns recent preprints starting from the given skip offset. +func (c *Client) RecentFrom(ctx context.Context, skip, limit int) ([]Preprint, int, error) { + return c.SearchFrom(ctx, "", skip, limit) } // GetPreprint fetches a single preprint by its ChemRxiv ID. diff --git a/chemrxiv/chemrxiv_test.go b/chemrxiv/chemrxiv_test.go index 59f59a2..441bdf2 100644 --- a/chemrxiv/chemrxiv_test.go +++ b/chemrxiv/chemrxiv_test.go @@ -413,3 +413,51 @@ func TestRecent(t *testing.T) { t.Error("Recent returned no preprints") } } + +func TestSearchFromSkip(t *testing.T) { + // Verify that SearchFrom sends the skip value in the query and adjusts rank. + var capturedQuery string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, _ := io.ReadAll(r.Body) + var body map[string]string + _ = json.Unmarshal(b, &body) + capturedQuery = body["query"] + _, _ = w.Write([]byte(sampleSearchResponse)) + })) + defer srv.Close() + + c := newTestClient(srv) + preprints, _, err := c.SearchFrom(context.Background(), "oxygen", 10, 5) + if err != nil { + t.Fatal(err) + } + // Query should contain skip:10 + if !strings.Contains(capturedQuery, "skip:10") { + t.Errorf("query does not contain skip:10; got: %s", capturedQuery) + } + // Ranks should start from skip+1 = 11 + if len(preprints) > 0 && preprints[0].Rank != 11 { + t.Errorf("first rank = %d, want 11 (skip=10)", preprints[0].Rank) + } +} + +func TestRateLimitedOn429(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTooManyRequests) + })) + defer srv.Close() + + cfg := DefaultConfig() + cfg.BaseURL = srv.URL + cfg.Rate = 0 + cfg.Retries = 2 + c := NewClient(cfg) + + _, _, err := c.Search(context.Background(), "test", 5) + if err == nil { + t.Fatal("expected error after 429 retries exhausted, got nil") + } + if !strings.Contains(err.Error(), "429") { + t.Errorf("error should mention 429, got: %v", err) + } +} diff --git a/cli/cmd_recent.go b/cli/cmd_recent.go index dab5e1a..bc79714 100644 --- a/cli/cmd_recent.go +++ b/cli/cmd_recent.go @@ -5,18 +5,21 @@ import ( ) func (a *App) recentCmd() *cobra.Command { - return &cobra.Command{ + var skip int + cmd := &cobra.Command{ Use: "recent", Short: "List the most recent ChemRxiv preprints", Long: "Fetch the most recently submitted ChemRxiv preprints. Default: up to 20 results.", RunE: func(cmd *cobra.Command, _ []string) error { n := a.effectiveLimit(20) - a.progressf("fetching %d recent preprints...", n) - preprints, _, err := a.client.Recent(cmd.Context(), n) + a.progressf("fetching %d recent preprints (skip %d)...", n, skip) + preprints, _, err := a.client.RecentFrom(cmd.Context(), skip, n) if err != nil { return mapFetchErr(err) } return a.renderOrEmpty(preprints, len(preprints)) }, } + cmd.Flags().IntVar(&skip, "skip", 0, "number of results to skip before returning") + return cmd } diff --git a/cli/cmd_search.go b/cli/cmd_search.go index 9965521..c160cce 100644 --- a/cli/cmd_search.go +++ b/cli/cmd_search.go @@ -5,7 +5,8 @@ import ( ) func (a *App) searchCmd() *cobra.Command { - return &cobra.Command{ + var skip int + cmd := &cobra.Command{ Use: "search ", Short: "Search ChemRxiv preprints by keyword", Long: `Search ChemRxiv preprints by keyword using the public GraphQL API. @@ -14,8 +15,8 @@ Paginates automatically until the requested limit is reached.`, RunE: func(cmd *cobra.Command, args []string) error { query := args[0] n := a.effectiveLimit(20) - a.progressf("searching for %q (limit %d)...", query, n) - preprints, total, err := a.client.Search(cmd.Context(), query, n) + a.progressf("searching for %q (skip %d, limit %d)...", query, skip, n) + preprints, total, err := a.client.SearchFrom(cmd.Context(), query, skip, n) if err != nil { return mapFetchErr(err) } @@ -23,4 +24,6 @@ Paginates automatically until the requested limit is reached.`, return a.renderOrEmpty(preprints, len(preprints)) }, } + cmd.Flags().IntVar(&skip, "skip", 0, "number of results to skip before returning") + return cmd } From 0395a002f0083ad6fc5d866de792c2f24e57319d Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:47:21 +0700 Subject: [PATCH 2/2] chore: upgrade GitHub Actions to latest versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node.js 20 is being deprecated in the Actions runtime. actions/checkout → v7.0.0 browser-actions/setup-chrome → v2.1.2 golangci/golangci-lint-action → v9.2.1 goreleaser/goreleaser-action → v7.2.2 docker/setup-qemu-action → v4.1.0 docker/setup-buildx-action → v4.1.0 docker/login-action → v4.2.0 sigstore/cosign-installer → v4.1.2 anchore/sbom-action → v0.24.0 --- .github/workflows/docs.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 501f402..bb2d8b1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,14 +26,14 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 with: submodules: true # Sitemap lastmod comes from the latest content commit. fetch-depth: 0 - name: Checkout tago - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: repository: tamnd/tago path: .tago-src @@ -107,7 +107,7 @@ jobs: group: cloudflare-pages-chemrxiv-cli cancel-in-progress: true steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 with: fetch-depth: 1 sparse-checkout: scripts/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e424c7..fcc24af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,7 +68,7 @@ jobs: # Tools GoReleaser shells out to for signing and SBOMs. - uses: sigstore/cosign-installer@v3 - - uses: anchore/sbom-action/download-syft@v0 + - uses: anchore/sbom-action/download-syft@v0.24.0 - uses: goreleaser/goreleaser-action@v6 with: