Skip to content

Commit 762d424

Browse files
authored
Fix NuGet search endpoints (#25613) (#26499)
Backport of #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
1 parent 3571cbb commit 762d424

File tree

6 files changed

+115
-21
lines changed

6 files changed

+115
-21
lines changed

models/db/common.go

+16
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,19 @@ func BuildCaseInsensitiveLike(key, value string) builder.Cond {
2020
}
2121
return builder.Like{"UPPER(" + key + ")", strings.ToUpper(value)}
2222
}
23+
24+
// BuilderDialect returns the xorm.Builder dialect of the engine
25+
func BuilderDialect() string {
26+
switch {
27+
case setting.Database.Type.IsMySQL():
28+
return builder.MYSQL
29+
case setting.Database.Type.IsSQLite3():
30+
return builder.SQLITE
31+
case setting.Database.Type.IsPostgreSQL():
32+
return builder.POSTGRES
33+
case setting.Database.Type.IsMSSQL():
34+
return builder.MSSQL
35+
default:
36+
return ""
37+
}
38+
}

models/packages/nuget/search.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package nuget
5+
6+
import (
7+
"context"
8+
"strings"
9+
10+
"code.gitea.io/gitea/models/db"
11+
packages_model "code.gitea.io/gitea/models/packages"
12+
13+
"xorm.io/builder"
14+
)
15+
16+
// SearchVersions gets all versions of packages matching the search options
17+
func SearchVersions(ctx context.Context, opts *packages_model.PackageSearchOptions) ([]*packages_model.PackageVersion, int64, error) {
18+
cond := toConds(opts)
19+
20+
e := db.GetEngine(ctx)
21+
22+
total, err := e.
23+
Where(cond).
24+
Count(&packages_model.Package{})
25+
if err != nil {
26+
return nil, 0, err
27+
}
28+
29+
inner := builder.
30+
Dialect(db.BuilderDialect()). // builder needs the sql dialect to build the Limit() below
31+
Select("*").
32+
From("package").
33+
Where(cond).
34+
OrderBy("package.name ASC")
35+
if opts.Paginator != nil {
36+
skip, take := opts.GetSkipTake()
37+
inner = inner.Limit(take, skip)
38+
}
39+
40+
sess := e.
41+
Where(opts.ToConds()).
42+
Table("package_version").
43+
Join("INNER", inner, "package.id = package_version.package_id")
44+
45+
pvs := make([]*packages_model.PackageVersion, 0, 10)
46+
return pvs, total, sess.Find(&pvs)
47+
}
48+
49+
// CountPackages counts all packages matching the search options
50+
func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOptions) (int64, error) {
51+
return db.GetEngine(ctx).
52+
Where(toConds(opts)).
53+
Count(&packages_model.Package{})
54+
}
55+
56+
func toConds(opts *packages_model.PackageSearchOptions) builder.Cond {
57+
var cond builder.Cond = builder.Eq{
58+
"package.is_internal": opts.IsInternal.IsTrue(),
59+
"package.owner_id": opts.OwnerID,
60+
"package.type": packages_model.TypeNuGet,
61+
}
62+
if opts.Name.Value != "" {
63+
if opts.Name.ExactMatch {
64+
cond = cond.And(builder.Eq{"package.lower_name": strings.ToLower(opts.Name.Value)})
65+
} else {
66+
cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.Name.Value)})
67+
}
68+
}
69+
return cond
70+
}

models/packages/package_version.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ type PackageSearchOptions struct {
189189
db.Paginator
190190
}
191191

192-
func (opts *PackageSearchOptions) toConds() builder.Cond {
192+
func (opts *PackageSearchOptions) ToConds() builder.Cond {
193193
cond := builder.NewCond()
194194
if !opts.IsInternal.IsNone() {
195195
cond = builder.Eq{
@@ -283,7 +283,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
283283
// SearchVersions gets all versions of packages matching the search options
284284
func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
285285
sess := db.GetEngine(ctx).
286-
Where(opts.toConds()).
286+
Where(opts.ToConds()).
287287
Table("package_version").
288288
Join("INNER", "package", "package.id = package_version.package_id")
289289

@@ -300,7 +300,7 @@ func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*Package
300300

301301
// SearchLatestVersions gets the latest version of every package matching the search options
302302
func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
303-
cond := opts.toConds().
303+
cond := opts.ToConds().
304304
And(builder.Expr("pv2.id IS NULL"))
305305

306306
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
328328
// ExistVersion checks if a version matching the search options exist
329329
func ExistVersion(ctx context.Context, opts *PackageSearchOptions) (bool, error) {
330330
return db.GetEngine(ctx).
331-
Where(opts.toConds()).
331+
Where(opts.ToConds()).
332332
Table("package_version").
333333
Join("INNER", "package", "package.id = package_version.package_id").
334334
Exist(new(PackageVersion))
@@ -337,7 +337,7 @@ func ExistVersion(ctx context.Context, opts *PackageSearchOptions) (bool, error)
337337
// CountVersions counts all versions of packages matching the search options
338338
func CountVersions(ctx context.Context, opts *PackageSearchOptions) (int64, error) {
339339
return db.GetEngine(ctx).
340-
Where(opts.toConds()).
340+
Where(opts.ToConds()).
341341
Table("package_version").
342342
Join("INNER", "package", "package.id = package_version.package_id").
343343
Count(new(PackageVersion))

routers/api/packages/nuget/api_v3.go

+11-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import (
99

1010
packages_model "code.gitea.io/gitea/models/packages"
1111
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
12+
13+
"golang.org/x/text/collate"
14+
"golang.org/x/text/language"
1215
)
1316

1417
// https://docs.microsoft.com/en-us/nuget/api/service-index#resources
@@ -207,9 +210,15 @@ func createSearchResultResponse(l *linkBuilder, totalHits int64, pds []*packages
207210
grouped[pd.Package.Name] = append(grouped[pd.Package.Name], pd)
208211
}
209212

213+
keys := make([]string, 0, len(grouped))
214+
for key := range grouped {
215+
keys = append(keys, key)
216+
}
217+
collate.New(language.English, collate.IgnoreCase).SortStrings(keys)
218+
210219
data := make([]*SearchResult, 0, len(pds))
211-
for _, group := range grouped {
212-
data = append(data, createSearchResult(l, group))
220+
for _, key := range keys {
221+
data = append(data, createSearchResult(l, grouped[key]))
213222
}
214223

215224
return &SearchResultResponse{

routers/api/packages/nuget/nuget.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"code.gitea.io/gitea/models/db"
1818
packages_model "code.gitea.io/gitea/models/packages"
19+
nuget_model "code.gitea.io/gitea/models/packages/nuget"
1920
"code.gitea.io/gitea/modules/context"
2021
"code.gitea.io/gitea/modules/log"
2122
packages_module "code.gitea.io/gitea/modules/packages"
@@ -115,7 +116,7 @@ func SearchServiceV2(ctx *context.Context) {
115116
skip, take := ctx.FormInt("$skip"), ctx.FormInt("$top")
116117
paginator := db.NewAbsoluteListOptions(skip, take)
117118

118-
pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
119+
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
119120
OwnerID: ctx.Package.Owner.ID,
120121
Type: packages_model.TypeNuGet,
121122
Name: packages_model.SearchValue{
@@ -166,9 +167,8 @@ func SearchServiceV2(ctx *context.Context) {
166167

167168
// 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
168169
func SearchServiceV2Count(ctx *context.Context) {
169-
count, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
170+
count, err := nuget_model.CountPackages(ctx, &packages_model.PackageSearchOptions{
170171
OwnerID: ctx.Package.Owner.ID,
171-
Type: packages_model.TypeNuGet,
172172
Name: packages_model.SearchValue{
173173
Value: getSearchTerm(ctx),
174174
},
@@ -184,9 +184,8 @@ func SearchServiceV2Count(ctx *context.Context) {
184184

185185
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
186186
func SearchServiceV3(ctx *context.Context) {
187-
pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
187+
pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
188188
OwnerID: ctx.Package.Owner.ID,
189-
Type: packages_model.TypeNuGet,
190189
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
191190
IsInternal: util.OptionalBoolFalse,
192191
Paginator: db.NewAbsoluteListOptions(

tests/integration/api_packages_nuget_test.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,10 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
414414
{"test", 1, 10, 1, 0},
415415
}
416416

417+
req := NewRequestWithBody(t, "PUT", url, createPackage(packageName, "1.0.99"))
418+
req = AddBasicAuthHeader(req, user.Name)
419+
MakeRequest(t, req, http.StatusCreated)
420+
417421
t.Run("v2", func(t *testing.T) {
418422
t.Run("Search()", func(t *testing.T) {
419423
defer tests.PrintCurrentTest(t)()
@@ -493,18 +497,14 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
493497
req = AddBasicAuthHeader(req, user.Name)
494498
MakeRequest(t, req, http.StatusCreated)
495499

496-
req = NewRequestWithBody(t, "PUT", url, createPackage(packageName, "1.0.99"))
497-
req = AddBasicAuthHeader(req, user.Name)
498-
MakeRequest(t, req, http.StatusCreated)
499-
500500
req = NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s", url, packageName))
501501
req = AddBasicAuthHeader(req, user.Name)
502502
resp := MakeRequest(t, req, http.StatusOK)
503503

504504
var result nuget.SearchResultResponse
505505
DecodeJSON(t, resp, &result)
506506

507-
assert.EqualValues(t, 3, result.TotalHits)
507+
assert.EqualValues(t, 2, result.TotalHits)
508508
assert.Len(t, result.Data, 2)
509509
for _, sr := range result.Data {
510510
if sr.ID == packageName {
@@ -517,12 +517,12 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
517517
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName+".dummy", "1.0.0"))
518518
req = AddBasicAuthHeader(req, user.Name)
519519
MakeRequest(t, req, http.StatusNoContent)
520-
521-
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, "1.0.99"))
522-
req = AddBasicAuthHeader(req, user.Name)
523-
MakeRequest(t, req, http.StatusNoContent)
524520
})
525521
})
522+
523+
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, "1.0.99"))
524+
req = AddBasicAuthHeader(req, user.Name)
525+
MakeRequest(t, req, http.StatusNoContent)
526526
})
527527

528528
t.Run("RegistrationService", func(t *testing.T) {

0 commit comments

Comments
 (0)