Fix NuGet search endpoints (#25613)
Fixes #25564 Fixes #23191 - Api v2 search endpoint should return only the latest version matching the query - Api v3 search endpoint should return `take` packages not package versions
This commit is contained in:
parent
56b6b2b88e
commit
ecd51f710b
6 changed files with 115 additions and 21 deletions
|
@ -37,3 +37,19 @@ func BuildCaseInsensitiveIn(key string, values []string) builder.Cond {
|
||||||
|
|
||||||
return builder.In("UPPER("+key+")", uppers)
|
return builder.In("UPPER("+key+")", uppers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuilderDialect returns the xorm.Builder dialect of the engine
|
||||||
|
func BuilderDialect() string {
|
||||||
|
switch {
|
||||||
|
case setting.Database.Type.IsMySQL():
|
||||||
|
return builder.MYSQL
|
||||||
|
case setting.Database.Type.IsSQLite3():
|
||||||
|
return builder.SQLITE
|
||||||
|
case setting.Database.Type.IsPostgreSQL():
|
||||||
|
return builder.POSTGRES
|
||||||
|
case setting.Database.Type.IsMSSQL():
|
||||||
|
return builder.MSSQL
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
70
models/packages/nuget/search.go
Normal file
70
models/packages/nuget/search.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package nuget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SearchVersions gets all versions of packages matching the search options
|
||||||
|
func SearchVersions(ctx context.Context, opts *packages_model.PackageSearchOptions) ([]*packages_model.PackageVersion, int64, error) {
|
||||||
|
cond := toConds(opts)
|
||||||
|
|
||||||
|
e := db.GetEngine(ctx)
|
||||||
|
|
||||||
|
total, err := e.
|
||||||
|
Where(cond).
|
||||||
|
Count(&packages_model.Package{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inner := builder.
|
||||||
|
Dialect(db.BuilderDialect()). // builder needs the sql dialect to build the Limit() below
|
||||||
|
Select("*").
|
||||||
|
From("package").
|
||||||
|
Where(cond).
|
||||||
|
OrderBy("package.name ASC")
|
||||||
|
if opts.Paginator != nil {
|
||||||
|
skip, take := opts.GetSkipTake()
|
||||||
|
inner = inner.Limit(take, skip)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := e.
|
||||||
|
Where(opts.ToConds()).
|
||||||
|
Table("package_version").
|
||||||
|
Join("INNER", inner, "package.id = package_version.package_id")
|
||||||
|
|
||||||
|
pvs := make([]*packages_model.PackageVersion, 0, 10)
|
||||||
|
return pvs, total, sess.Find(&pvs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountPackages counts all packages matching the search options
|
||||||
|
func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOptions) (int64, error) {
|
||||||
|
return db.GetEngine(ctx).
|
||||||
|
Where(toConds(opts)).
|
||||||
|
Count(&packages_model.Package{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func toConds(opts *packages_model.PackageSearchOptions) builder.Cond {
|
||||||
|
var cond builder.Cond = builder.Eq{
|
||||||
|
"package.is_internal": opts.IsInternal.IsTrue(),
|
||||||
|
"package.owner_id": opts.OwnerID,
|
||||||
|
"package.type": packages_model.TypeNuGet,
|
||||||
|
}
|
||||||
|
if opts.Name.Value != "" {
|
||||||
|
if opts.Name.ExactMatch {
|
||||||
|
cond = cond.And(builder.Eq{"package.lower_name": strings.ToLower(opts.Name.Value)})
|
||||||
|
} else {
|
||||||
|
cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.Name.Value)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cond
|
||||||
|
}
|
|
@ -189,7 +189,7 @@ type PackageSearchOptions struct {
|
||||||
db.Paginator
|
db.Paginator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *PackageSearchOptions) toConds() builder.Cond {
|
func (opts *PackageSearchOptions) ToConds() builder.Cond {
|
||||||
cond := builder.NewCond()
|
cond := builder.NewCond()
|
||||||
if !opts.IsInternal.IsNone() {
|
if !opts.IsInternal.IsNone() {
|
||||||
cond = builder.Eq{
|
cond = builder.Eq{
|
||||||
|
@ -283,7 +283,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
|
||||||
// SearchVersions gets all versions of packages matching the search options
|
// SearchVersions gets all versions of packages matching the search options
|
||||||
func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
|
func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
|
||||||
sess := db.GetEngine(ctx).
|
sess := db.GetEngine(ctx).
|
||||||
Where(opts.toConds()).
|
Where(opts.ToConds()).
|
||||||
Table("package_version").
|
Table("package_version").
|
||||||
Join("INNER", "package", "package.id = package_version.package_id")
|
Join("INNER", "package", "package.id = package_version.package_id")
|
||||||
|
|
||||||
|
@ -300,7 +300,7 @@ func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*Package
|
||||||
|
|
||||||
// SearchLatestVersions gets the latest version of every package matching the search options
|
// SearchLatestVersions gets the latest version of every package matching the search options
|
||||||
func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
|
func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
|
||||||
cond := opts.toConds().
|
cond := opts.ToConds().
|
||||||
And(builder.Expr("pv2.id IS NULL"))
|
And(builder.Expr("pv2.id IS NULL"))
|
||||||
|
|
||||||
joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))")
|
joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))")
|
||||||
|
@ -328,7 +328,7 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
|
||||||
// ExistVersion checks if a version matching the search options exist
|
// ExistVersion checks if a version matching the search options exist
|
||||||
func ExistVersion(ctx context.Context, opts *PackageSearchOptions) (bool, error) {
|
func ExistVersion(ctx context.Context, opts *PackageSearchOptions) (bool, error) {
|
||||||
return db.GetEngine(ctx).
|
return db.GetEngine(ctx).
|
||||||
Where(opts.toConds()).
|
Where(opts.ToConds()).
|
||||||
Table("package_version").
|
Table("package_version").
|
||||||
Join("INNER", "package", "package.id = package_version.package_id").
|
Join("INNER", "package", "package.id = package_version.package_id").
|
||||||
Exist(new(PackageVersion))
|
Exist(new(PackageVersion))
|
||||||
|
@ -337,7 +337,7 @@ func ExistVersion(ctx context.Context, opts *PackageSearchOptions) (bool, error)
|
||||||
// CountVersions counts all versions of packages matching the search options
|
// CountVersions counts all versions of packages matching the search options
|
||||||
func CountVersions(ctx context.Context, opts *PackageSearchOptions) (int64, error) {
|
func CountVersions(ctx context.Context, opts *PackageSearchOptions) (int64, error) {
|
||||||
return db.GetEngine(ctx).
|
return db.GetEngine(ctx).
|
||||||
Where(opts.toConds()).
|
Where(opts.ToConds()).
|
||||||
Table("package_version").
|
Table("package_version").
|
||||||
Join("INNER", "package", "package.id = package_version.package_id").
|
Join("INNER", "package", "package.id = package_version.package_id").
|
||||||
Count(new(PackageVersion))
|
Count(new(PackageVersion))
|
||||||
|
|
|
@ -9,6 +9,9 @@ import (
|
||||||
|
|
||||||
packages_model "code.gitea.io/gitea/models/packages"
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
||||||
|
|
||||||
|
"golang.org/x/text/collate"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/nuget/api/service-index#resources
|
// https://docs.microsoft.com/en-us/nuget/api/service-index#resources
|
||||||
|
@ -207,9 +210,15 @@ func createSearchResultResponse(l *linkBuilder, totalHits int64, pds []*packages
|
||||||
grouped[pd.Package.Name] = append(grouped[pd.Package.Name], pd)
|
grouped[pd.Package.Name] = append(grouped[pd.Package.Name], pd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(grouped))
|
||||||
|
for key := range grouped {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
collate.New(language.English, collate.IgnoreCase).SortStrings(keys)
|
||||||
|
|
||||||
data := make([]*SearchResult, 0, len(pds))
|
data := make([]*SearchResult, 0, len(pds))
|
||||||
for _, group := range grouped {
|
for _, key := range keys {
|
||||||
data = append(data, createSearchResult(l, group))
|
data = append(data, createSearchResult(l, grouped[key]))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SearchResultResponse{
|
return &SearchResultResponse{
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
packages_model "code.gitea.io/gitea/models/packages"
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
|
nuget_model "code.gitea.io/gitea/models/packages/nuget"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
packages_module "code.gitea.io/gitea/modules/packages"
|
packages_module "code.gitea.io/gitea/modules/packages"
|
||||||
|
@ -115,7 +116,7 @@ func SearchServiceV2(ctx *context.Context) {
|
||||||
skip, take := ctx.FormInt("$skip"), ctx.FormInt("$top")
|
skip, take := ctx.FormInt("$skip"), ctx.FormInt("$top")
|
||||||
paginator := db.NewAbsoluteListOptions(skip, take)
|
paginator := db.NewAbsoluteListOptions(skip, take)
|
||||||
|
|
||||||
pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||||
OwnerID: ctx.Package.Owner.ID,
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
Type: packages_model.TypeNuGet,
|
Type: packages_model.TypeNuGet,
|
||||||
Name: packages_model.SearchValue{
|
Name: packages_model.SearchValue{
|
||||||
|
@ -166,9 +167,8 @@ func SearchServiceV2(ctx *context.Context) {
|
||||||
|
|
||||||
// http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752351
|
// http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752351
|
||||||
func SearchServiceV2Count(ctx *context.Context) {
|
func SearchServiceV2Count(ctx *context.Context) {
|
||||||
count, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
|
count, err := nuget_model.CountPackages(ctx, &packages_model.PackageSearchOptions{
|
||||||
OwnerID: ctx.Package.Owner.ID,
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
Type: packages_model.TypeNuGet,
|
|
||||||
Name: packages_model.SearchValue{
|
Name: packages_model.SearchValue{
|
||||||
Value: getSearchTerm(ctx),
|
Value: getSearchTerm(ctx),
|
||||||
},
|
},
|
||||||
|
@ -184,9 +184,8 @@ func SearchServiceV2Count(ctx *context.Context) {
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
|
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
|
||||||
func SearchServiceV3(ctx *context.Context) {
|
func SearchServiceV3(ctx *context.Context) {
|
||||||
pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||||
OwnerID: ctx.Package.Owner.ID,
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
Type: packages_model.TypeNuGet,
|
|
||||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||||
IsInternal: util.OptionalBoolFalse,
|
IsInternal: util.OptionalBoolFalse,
|
||||||
Paginator: db.NewAbsoluteListOptions(
|
Paginator: db.NewAbsoluteListOptions(
|
||||||
|
|
|
@ -414,6 +414,10 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
{"test", 1, 10, 1, 0},
|
{"test", 1, 10, 1, 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req := NewRequestWithBody(t, "PUT", url, createPackage(packageName, "1.0.99"))
|
||||||
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
t.Run("v2", func(t *testing.T) {
|
t.Run("v2", func(t *testing.T) {
|
||||||
t.Run("Search()", func(t *testing.T) {
|
t.Run("Search()", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
@ -493,10 +497,6 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
req = AddBasicAuthHeader(req, user.Name)
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
MakeRequest(t, req, http.StatusCreated)
|
MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
req = NewRequestWithBody(t, "PUT", url, createPackage(packageName, "1.0.99"))
|
|
||||||
req = AddBasicAuthHeader(req, user.Name)
|
|
||||||
MakeRequest(t, req, http.StatusCreated)
|
|
||||||
|
|
||||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s", url, packageName))
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s", url, packageName))
|
||||||
req = AddBasicAuthHeader(req, user.Name)
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
@ -504,7 +504,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
var result nuget.SearchResultResponse
|
var result nuget.SearchResultResponse
|
||||||
DecodeJSON(t, resp, &result)
|
DecodeJSON(t, resp, &result)
|
||||||
|
|
||||||
assert.EqualValues(t, 3, result.TotalHits)
|
assert.EqualValues(t, 2, result.TotalHits)
|
||||||
assert.Len(t, result.Data, 2)
|
assert.Len(t, result.Data, 2)
|
||||||
for _, sr := range result.Data {
|
for _, sr := range result.Data {
|
||||||
if sr.ID == packageName {
|
if sr.ID == packageName {
|
||||||
|
@ -517,13 +517,13 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName+".dummy", "1.0.0"))
|
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName+".dummy", "1.0.0"))
|
||||||
req = AddBasicAuthHeader(req, user.Name)
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
MakeRequest(t, req, http.StatusNoContent)
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, "1.0.99"))
|
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, "1.0.99"))
|
||||||
req = AddBasicAuthHeader(req, user.Name)
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
MakeRequest(t, req, http.StatusNoContent)
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("RegistrationService", func(t *testing.T) {
|
t.Run("RegistrationService", func(t *testing.T) {
|
||||||
indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName)
|
indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName)
|
||||||
|
|
Loading…
Reference in a new issue