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
					
				| @ -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)() | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user