mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08: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.
 | 
					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 New Issue
	
	Block a user