mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-03 21:08:25 +00:00 
			
		
		
		
	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:
		@@ -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
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -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"`
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	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)()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user