NPM Package Registry search API endpoint (#20280)

Close #20098, in the NPM registry API, implemented to match what's described by https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#get-v1search

Currently have only implemented the bare minimum to work with the [Unity Package Manager](https://docs.unity3d.com/Manual/upm-ui.html).

Co-authored-by: Jack Vine <jackv@jack-lemur-suse.cat-prometheus.ts.net>
Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Jack Vine 2022-09-24 20:54:33 +09:30 committed by GitHub
parent da0a9ec811
commit 83680c97a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 134 additions and 0 deletions

View file

@ -127,6 +127,10 @@ npm dist-tag add test_package@1.0.2 release
The tag name must not be a valid version. All tag names which are parsable as a version are rejected. The tag name must not be a valid version. All tag names which are parsable as a version are rejected.
## Search packages
The registry supports [searching](https://docs.npmjs.com/cli/v7/commands/npm-search/) but does not support special search qualifiers like `author:gitea`.
## Supported commands ## Supported commands
``` ```
@ -136,4 +140,5 @@ npm publish
npm unpublish npm unpublish
npm dist-tag npm dist-tag
npm view npm view
npm search
``` ```

View file

@ -96,6 +96,34 @@ type PackageDistribution struct {
NpmSignature string `json:"npm-signature,omitempty"` NpmSignature string `json:"npm-signature,omitempty"`
} }
type PackageSearch struct {
Objects []*PackageSearchObject `json:"objects"`
Total int64 `json:"total"`
}
type PackageSearchObject struct {
Package *PackageSearchPackage `json:"package"`
}
type PackageSearchPackage struct {
Scope string `json:"scope"`
Name string `json:"name"`
Version string `json:"version"`
Date time.Time `json:"date"`
Description string `json:"description"`
Author User `json:"author"`
Publisher User `json:"publisher"`
Maintainers []User `json:"maintainers"`
Keywords []string `json:"keywords,omitempty"`
Links *PackageSearchPackageLinks `json:"links"`
}
type PackageSearchPackageLinks struct {
Registry string `json:"npm"`
Homepage string `json:"homepage,omitempty"`
Repository string `json:"repository,omitempty"`
}
// User https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package // User https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
type User struct { type User struct {
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`

View file

@ -236,6 +236,9 @@ func Routes(ctx gocontext.Context) *web.Route {
r.Delete("", npm.DeletePackageTag) r.Delete("", npm.DeletePackageTag)
}, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeWrite))
}) })
r.Group("/-/v1/search", func() {
r.Get("", npm.PackageSearch)
})
}) })
r.Group("/pub", func() { r.Group("/pub", func() {
r.Group("/api/packages", func() { r.Group("/api/packages", func() {

View file

@ -74,3 +74,38 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
}, },
} }
} }
func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total int64) *npm_module.PackageSearch {
objects := make([]*npm_module.PackageSearchObject, 0, len(pds))
for _, pd := range pds {
metadata := pd.Metadata.(*npm_module.Metadata)
scope := metadata.Scope
if scope == "" {
scope = "unscoped"
}
objects = append(objects, &npm_module.PackageSearchObject{
Package: &npm_module.PackageSearchPackage{
Scope: scope,
Name: metadata.Name,
Version: pd.Version.Version,
Date: pd.Version.CreatedUnix.AsLocalTime(),
Description: metadata.Description,
Author: npm_module.User{Name: metadata.Author},
Publisher: npm_module.User{Name: pd.Owner.Name},
Maintainers: []npm_module.User{}, // npm cli needs this field
Keywords: metadata.Keywords,
Links: &npm_module.PackageSearchPackageLinks{
Registry: pd.FullWebLink(),
Homepage: metadata.ProjectURL,
},
},
})
}
return &npm_module.PackageSearch{
Objects: objects,
Total: total,
}
}

View file

@ -350,3 +350,35 @@ func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly boo
return committer.Commit() return committer.Commit()
} }
func PackageSearch(ctx *context.Context) {
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNpm,
Name: packages_model.SearchValue{
ExactMatch: false,
Value: ctx.FormTrim("text"),
},
Paginator: db.NewAbsoluteListOptions(
ctx.FormInt("from"),
ctx.FormInt("size"),
),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
resp := createPackageSearchResponse(
pds,
total,
)
ctx.JSON(http.StatusOK, resp)
}

View file

@ -224,6 +224,37 @@ func TestPackageNpm(t *testing.T) {
test(t, http.StatusOK, packageTag2) test(t, http.StatusOK, packageTag2)
}) })
t.Run("Search", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
url := fmt.Sprintf("/api/packages/%s/npm/-/v1/search", user.Name)
cases := []struct {
Query string
Skip int
Take int
ExpectedTotal int64
ExpectedResults int
}{
{"", 0, 0, 1, 1},
{"", 0, 10, 1, 1},
{"gitea", 0, 10, 0, 0},
{"test", 0, 10, 1, 1},
{"test", 1, 10, 1, 0},
}
for i, c := range cases {
req := NewRequest(t, "GET", fmt.Sprintf("%s?text=%s&from=%d&size=%d", url, c.Query, c.Skip, c.Take))
resp := MakeRequest(t, req, http.StatusOK)
var result npm.PackageSearch
DecodeJSON(t, resp, &result)
assert.Equal(t, c.ExpectedTotal, result.Total, "case %d: unexpected total hits", i)
assert.Len(t, result.Objects, c.ExpectedResults, "case %d: unexpected result count", i)
}
})
t.Run("Delete", func(t *testing.T) { t.Run("Delete", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()