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:
parent
da0a9ec811
commit
83680c97a7
6 changed files with 134 additions and 0 deletions
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)()
|
||||||
|
|
||||||
|
|
Reference in a new issue