[pkgsite] internal: define API handler for MajorVersions

1 view
Skip to first unread message

Ethan Lee (Gerrit)

unread,
Dec 16, 2025, 12:46:53 PM (2 days ago) Dec 16
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Ethan Lee has uploaded the change for review

Commit message

internal: define API handler for MajorVersions
Change-Id: Ic02b31d073731ff62ec8b41ebdc170efb6d47fdc

Change diff

diff --git a/internal/frontend/api/handlers.go b/internal/frontend/api/handlers.go
index ec3ac13..ff2e6d9 100644
--- a/internal/frontend/api/handlers.go
+++ b/internal/frontend/api/handlers.go
@@ -7,6 +7,7 @@
import (
"encoding/json"
"net/http"
+ "strconv"
"time"

"golang.org/x/pkgsite/internal"
@@ -64,7 +65,21 @@
return &serrors.ServerError{Status: http.StatusInternalServerError, ResponseText: "Internal server error"}
}

- versions, err := db.GetVersionsForPath(r.Context(), req.ModulePath)
+ offset, err := strconv.Atoi(req.From)
+ if err != nil && req.From != "" {
+ return &serrors.ServerError{Status: http.StatusBadRequest, ResponseText: "Invalid from value"}
+ }
+ limit := req.Max
+ if limit <= 0 {
+ limit = 100
+ }
+
+ versions, next, err := db.GetVersionsForPathWithPagination(r.Context(), req.ModulePath, limit, offset)
+ if err != nil {
+ return &serrors.ServerError{Status: http.StatusInternalServerError, ResponseText: "Internal server error"}
+ }
+
+ total, err := db.GetNumVersionsForPath(r.Context(), req.ModulePath)
if err != nil {
return &serrors.ServerError{Status: http.StatusInternalServerError, ResponseText: "Internal server error"}
}
@@ -78,10 +93,16 @@
})
}

+ var nextToken string
+ if next > 0 {
+ nextToken = strconv.Itoa(next)
+ }
+
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(&MajorVersions{
Versions: mvs,
- Total: len(mvs),
+ Total: total,
+ Next: nextToken,
}); err != nil {
return &serrors.ServerError{Status: http.StatusInternalServerError, ResponseText: "Internal server error"}
}
diff --git a/internal/frontend/api/handlers_test.go b/internal/frontend/api/handlers_test.go
index bbd38b6..ed0aaa6 100644
--- a/internal/frontend/api/handlers_test.go
+++ b/internal/frontend/api/handlers_test.go
@@ -109,8 +109,9 @@

func TestHandleMajorVersions(t *testing.T) {
versions := []*internal.ModuleInfo{
- {ModulePath: "example.com/module", Version: "v1.2.3", CommitTime: time.Now()},
+ {ModulePath: "example.com/module/v3", Version: "v3.0.0", CommitTime: time.Now()},
{ModulePath: "example.com/module/v2", Version: "v2.0.0", CommitTime: time.Now()},
+ {ModulePath: "example.com/module", Version: "v1.2.3", CommitTime: time.Now()},
}
ds := fakedatasource.New()
for _, v := range versions {
@@ -125,27 +126,60 @@
},
})
}
- reqBody := &MajorVersionsRequest{ModulePath: "example.com/module"}
- body, err := json.Marshal(reqBody)
- if err != nil {
- t.Fatal(err)
- }
- req := httptest.NewRequest(http.MethodPost, "/majorversions", bytes.NewReader(body))
- w := httptest.NewRecorder()
- err = HandleMajorVersions(w, req, ds)
- if err != nil {
- t.Fatal(err)
- }
- if w.Code != http.StatusOK {
- t.Errorf("got status %d; want %d", w.Code, http.StatusOK)
- }
- var got MajorVersions
- if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
- t.Fatal(err)
- }
- if got.Total != len(versions) {
- t.Errorf("got %d total versions; want %d", got.Total, len(versions))
- }
+
+ t.Run("no pagination", func(t *testing.T) {
+ reqBody := &MajorVersionsRequest{ModulePath: "example.com/module"}
+ body, err := json.Marshal(reqBody)
+ if err != nil {
+ t.Fatal(err)
+ }
+ req := httptest.NewRequest(http.MethodPost, "/majorversions", bytes.NewReader(body))
+ w := httptest.NewRecorder()
+ err = HandleMajorVersions(w, req, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if w.Code != http.StatusOK {
+ t.Errorf("got status %d; want %d", w.Code, http.StatusOK)
+ }
+ var got MajorVersions
+ if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
+ t.Fatal(err)
+ }
+ if got.Total != len(versions) {
+ t.Errorf("got %d total versions; want %d", got.Total, len(versions))
+ }
+ })
+
+ t.Run("pagination", func(t *testing.T) {
+ reqBody := &MajorVersionsRequest{ModulePath: "example.com/module", Max: 1, From: "1"}
+ body, err := json.Marshal(reqBody)
+ if err != nil {
+ t.Fatal(err)
+ }
+ req := httptest.NewRequest(http.MethodPost, "/majorversions", bytes.NewReader(body))
+ w := httptest.NewRecorder()
+ err = HandleMajorVersions(w, req, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if w.Code != http.StatusOK {
+ t.Errorf("got status %d; want %d", w.Code, http.StatusOK)
+ }
+ var got MajorVersions
+ if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
+ t.Fatal(err)
+ }
+ if got.Total != len(versions) {
+ t.Errorf("got %d total versions; want %d", got.Total, len(versions))
+ }
+ if len(got.Versions) != 1 {
+ t.Errorf("got %d versions; want 1", len(got.Versions))
+ }
+ if got.Next != "2" {
+ t.Errorf("got next %q; want %q", got.Next, "2")
+ }
+ })
}
func TestHandleMajorVersions_InvalidPayload(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/majorversions", bytes.NewReader([]byte("invalid json")))
diff --git a/internal/interfaces.go b/internal/interfaces.go
index 5871478..d55a77b 100644
--- a/internal/interfaces.go
+++ b/internal/interfaces.go
@@ -21,6 +21,8 @@
GetVersionMap(ctx context.Context, modulePath, requestedVersion string) (_ *VersionMap, err error)
GetVersionMaps(ctx context.Context, paths []string, requestedVersion string) (_ []*VersionMap, err error)
GetVersionsForPath(ctx context.Context, path string) (_ []*ModuleInfo, err error)
+ GetVersionsForPathWithPagination(ctx context.Context, path string, limit int, offset int) (versions []*ModuleInfo, nextPageToken int, err error)
+ GetNumVersionsForPath(ctx context.Context, path string) (int, error)
InsertModule(ctx context.Context, m *Module, lmv *LatestModuleVersions) (isLatest bool, err error)
UpsertVersionMap(ctx context.Context, vm *VersionMap) (err error)
}
diff --git a/internal/postgres/version.go b/internal/postgres/version.go
index 602c322..b0fb7fa 100644
--- a/internal/postgres/version.go
+++ b/internal/postgres/version.go
@@ -44,6 +44,34 @@
return versions, nil
}

+func (db *DB) GetNumVersionsForPath(ctx context.Context, path string) (int, error) {
+ defer derrors.WrapStack(&err, "GetNumVersionsForPath(ctx, %q)", path)
+ defer stats.Elapsed(ctx, "GetNumVersionsForPath")()
+
+ query := `
+ SELECT
+ COUNT(*)
+ FROM modules m
+ INNER JOIN units u
+ ON u.module_id = m.id
+ WHERE
+ u.v1path_id = (
+ SELECT u2.v1path_id
+ FROM units as u2
+ INNER JOIN paths p
+ ON p.id = u2.path_id
+ WHERE p.path = $1
+ LIMIT 1
+ )`
+
+ var count int
+ err := db.db.QueryRow(ctx, query, path).Scan(&count)
+ if err != nil {
+ return 0, err
+ }
+ return count, nil
+}
+
// getPathVersions returns a list of versions sorted in descending semver
// order. The version types included in the list are specified by a list of
// VersionTypes.
@@ -109,6 +137,86 @@
return versions, nil
}

+// GetVersionsForPathWithPagination returns a list of tagged versions sorted in
+// descending semver order if any exist. If none, it returns the 10 most
+// recent from a list of pseudo-versions sorted in descending semver order.
+func (db *DB) GetVersionsForPathWithPagination(ctx context.Context, path string, limit, offset int) (_ []*internal.ModuleInfo, nextPageToken int, err error) {
+ defer derrors.WrapStack(&err, "GetVersionsForPathWithPagination(ctx, %q, %d, %d)", path, limit, offset)
+ defer stats.Elapsed(ctx, "GetVersionsForPathWithPagination")()
+
+ versions, err := getPathVersionsWithPagination(ctx, db, path, limit, offset, version.TypeRelease, version.TypePrerelease)
+ if err != nil {
+ return nil, 0, err
+ }
+ if len(versions) != 0 {
+ if len(versions) < limit {
+ return versions, 0, nil
+ }
+ return versions, offset + len(versions), nil
+ }
+ versions, err = getPathVersionsWithPagination(ctx, db, path, limit, offset, version.TypePseudo)
+ if err != nil {
+ return nil, 0, err
+ }
+ if len(versions) < limit {
+ return versions, 0, nil
+ }
+ return versions, offset + len(versions), nil
+}
+
+// getPathVersionsWithPagination returns a list of versions sorted in descending semver
+// order. The version types included in the list are specified by a list of
+// VersionTypes.
+func getPathVersionsWithPagination(ctx context.Context, db *DB, path string, limit, offset int, versionTypes ...version.Type) (_ []*internal.ModuleInfo, err error) {
+ defer derrors.WrapStack(&err, "getPathVersionsWithPagination(ctx, db, %q, %v)", path, versionTypes)
+
+ baseQuery := `
+ SELECT
+ m.module_path,
+ m.version,
+ m.commit_time,
+ m.redistributable,
+ m.has_go_mod,
+ m.source_info
+ FROM modules m
+ INNER JOIN units u
+ ON u.module_id = m.id
+ WHERE
+ u.v1path_id = (
+ SELECT u2.v1path_id
+ FROM units as u2
+ INNER JOIN paths p
+ ON p.id = u2.path_id
+ WHERE p.path = $1
+ LIMIT 1
+ )
+ AND version_type in (%s)
+ ORDER BY
+ m.incompatible,
+ m.module_path DESC,
+ m.sort_version DESC
+ LIMIT $2
+ OFFSET $3`
+
+ query := fmt.Sprintf(baseQuery, versionTypeExpr(versionTypes))
+ var versions []*internal.ModuleInfo
+ collect := func(rows *sql.Rows) error {
+ mi, err := scanModuleInfo(rows.Scan)
+ if err != nil {
+ return fmt.Errorf("row.Scan(): %v", err)
+ }
+ versions = append(versions, mi)
+ return nil
+ }
+ if err := db.db.RunQuery(ctx, query, collect, path, limit, offset); err != nil {
+ return nil, err
+ }
+ if err := populateLatestInfos(ctx, db, versions); err != nil {
+ return nil, err
+ }
+ return versions, nil
+}
+
// versionTypeExpr returns a comma-separated list of version types,
// for use in a clause like "WHERE version_type IN (%s)"
func versionTypeExpr(vts []version.Type) string {
diff --git a/internal/testing/fakedatasource/fakedatasource.go b/internal/testing/fakedatasource/fakedatasource.go
index 1f69519..16c524b 100644
--- a/internal/testing/fakedatasource/fakedatasource.go
+++ b/internal/testing/fakedatasource/fakedatasource.go
@@ -425,6 +425,37 @@
return infos, nil
}

+func (ds *FakeDataSource) GetVersionsForPathWithPagination(ctx context.Context, path string, limit, offset int) ([]*internal.ModuleInfo, int, error) {
+ infos, err := ds.GetVersionsForPath(ctx, path)
+ if err != nil {
+ return nil, 0, err
+ }
+ start := offset
+ if start > len(infos) {
+ start = len(infos)
+ }
+ end := start + limit
+ if end > len(infos) {
+ end = len(infos)
+ }
+ if limit <= 0 {
+ end = len(infos)
+ }
+ next := end
+ if next >= len(infos) {
+ next = 0
+ }
+ return infos[start:end], next, nil
+}
+
+func (ds *FakeDataSource) GetNumVersionsForPath(ctx context.Context, path string) (int, error) {
+ infos, err := ds.GetVersionsForPath(ctx, path)
+ if err != nil {
+ return 0, err
+ }
+ return len(infos), nil
+}
+
// trimSlashVersionPrefix trims a /vN path component prefix if one is present in path,
// and returns path unchanged otherwise.
func trimSlashVersionPrefix(path string) string {

Change information

Files:
  • M internal/frontend/api/handlers.go
  • M internal/frontend/api/handlers_test.go
  • M internal/interfaces.go
  • M internal/postgres/version.go
  • M internal/testing/fakedatasource/fakedatasource.go
Change size: M
Delta: 5 files changed, 220 insertions(+), 24 deletions(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
  • requirement is not satisfiedkokoro-CI-Passes
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: pkgsite
Gerrit-Branch: master
Gerrit-Change-Id: Ic02b31d073731ff62ec8b41ebdc170efb6d47fdc
Gerrit-Change-Number: 730481
Gerrit-PatchSet: 1
Gerrit-Owner: Ethan Lee <etha...@google.com>
unsatisfied_requirement
satisfied_requirement
open
diffy
Reply all
Reply to author
Forward
0 new messages