diff --git a/docs/content/doc/packages/npm.en-us.md b/docs/content/doc/packages/npm.en-us.md index 122f306ee5e3a..decb3f743c419 100644 --- a/docs/content/doc/packages/npm.en-us.md +++ b/docs/content/doc/packages/npm.en-us.md @@ -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. +## 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 ``` @@ -136,4 +140,5 @@ npm publish npm unpublish npm dist-tag npm view +npm search ``` diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 88ce55ecdbef8..2ed4d262481d1 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -96,6 +96,34 @@ type PackageDistribution struct { 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 type User struct { Username string `json:"username,omitempty"` diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 93f9afca7b283..cb9b3b78abd4c 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -236,6 +236,9 @@ func Routes(ctx gocontext.Context) *web.Route { r.Delete("", npm.DeletePackageTag) }, reqPackageAccess(perm.AccessModeWrite)) }) + r.Group("/-/v1/search", func() { + r.Get("", npm.PackageSearch) + }) }) r.Group("/pub", func() { r.Group("/api/packages", func() { diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go index 763c595152ec1..d1027763a836a 100644 --- a/routers/api/packages/npm/api.go +++ b/routers/api/packages/npm/api.go @@ -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, + } +} diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 66b999d47ee50..2989ce6e7f6bd 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -350,3 +350,35 @@ func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly boo 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) +} diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go index fe6cea1cb68da..14ed681be3a7a 100644 --- a/tests/integration/api_packages_npm_test.go +++ b/tests/integration/api_packages_npm_test.go @@ -224,6 +224,37 @@ func TestPackageNpm(t *testing.T) { 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) { defer tests.PrintCurrentTest(t)()