Merge pull request #14369 from ywk253100/210303_sort

Introduce "sort" in query to provide a general solution for sorting
This commit is contained in:
Wenkai Yin(尹文开) 2021-03-11 09:28:34 +08:00 committed by GitHub
commit 4ef93565f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 700 additions and 540 deletions

View File

@ -153,6 +153,7 @@ paths:
- $ref: '#/parameters/requestId' - $ref: '#/parameters/requestId'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- $ref: '#/parameters/sort'
- name: name - name: name
in: query in: query
description: The name of project. description: The name of project.
@ -372,6 +373,7 @@ paths:
- $ref: '#/parameters/requestId' - $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName' - $ref: '#/parameters/projectName'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
responses: responses:
@ -488,6 +490,7 @@ paths:
- $ref: '#/parameters/projectName' - $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName' - $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- $ref: '#/parameters/acceptVulnerabilities' - $ref: '#/parameters/acceptVulnerabilities'
@ -762,6 +765,7 @@ paths:
- $ref: '#/parameters/repositoryName' - $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/reference' - $ref: '#/parameters/reference'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- name: with_signature - name: with_signature
@ -1059,6 +1063,7 @@ paths:
parameters: parameters:
- $ref: '#/parameters/requestId' - $ref: '#/parameters/requestId'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
responses: responses:
@ -1092,6 +1097,7 @@ paths:
- $ref: '#/parameters/projectName' - $ref: '#/parameters/projectName'
- $ref: '#/parameters/requestId' - $ref: '#/parameters/requestId'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
responses: responses:
@ -1179,6 +1185,7 @@ paths:
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
responses: responses:
'200': '200':
description: Success description: Success
@ -1346,6 +1353,7 @@ paths:
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
responses: responses:
'200': '200':
description: List preheat policies success description: List preheat policies success
@ -1491,6 +1499,7 @@ paths:
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
responses: responses:
'200': '200':
description: List executions success description: List executions success
@ -1587,6 +1596,7 @@ paths:
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
responses: responses:
'200': '200':
description: List tasks success description: List tasks success
@ -1683,6 +1693,7 @@ paths:
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
tags: tags:
- robotv1 - robotv1
operationId: ListRobotV1 operationId: ListRobotV1
@ -1840,6 +1851,7 @@ paths:
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
responses: responses:
'200': '200':
description: Success description: Success
@ -1970,6 +1982,7 @@ paths:
parameters: parameters:
- $ref: '#/parameters/requestId' - $ref: '#/parameters/requestId'
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
responses: responses:
@ -2249,6 +2262,7 @@ paths:
- replication - replication
operationId: listReplicationExecutions operationId: listReplicationExecutions
parameters: parameters:
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- name: policy_id - name: policy_id
@ -2369,6 +2383,7 @@ paths:
- replication - replication
operationId: listReplicationTasks operationId: listReplicationTasks
parameters: parameters:
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
- name: id - name: id
@ -2583,6 +2598,7 @@ paths:
operationId: getGCHistory operationId: getGCHistory
parameters: parameters:
- $ref: '#/parameters/query' - $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize' - $ref: '#/parameters/pageSize'
responses: responses:
@ -3363,6 +3379,29 @@ parameters:
in: query in: query
type: string type: string
required: false required: false
sort:
name: sort
description: Sort the resource list in ascending or descending order. e.g. sort by field1 in ascending orderr and field2 in descending order with "sort=field1,-field2"
in: query
type: string
required: false
page:
name: page
in: query
type: integer
format: int64
required: false
description: The page number
default: 1
pageSize:
name: page_size
in: query
type: integer
format: int64
required: false
description: The size of per page
default: 10
maximum: 100
requestId: requestId:
name: X-Request-Id name: X-Request-Id
description: An unique ID for the request description: An unique ID for the request
@ -3428,29 +3467,6 @@ parameters:
description: The name of the tag description: The name of the tag
required: true required: true
type: string type: string
page:
name: page
in: query
type: integer
format: int64
required: false
description: The page number
default: 1
pageSize:
name: page_size
in: query
type: integer
format: int64
required: false
description: The size of per page
default: 10
maximum: 100
sort:
name: sort
in: query
type: string
required: false
description: The order by fields of the query, the format is '+field1,-field2'.
instanceName: instanceName:
name: preheat_instance_name name: preheat_instance_name
in: path in: path

View File

@ -38,7 +38,7 @@ const (
type Project struct { type Project struct {
ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"` ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"`
OwnerID int `orm:"column(owner_id)" json:"owner_id"` OwnerID int `orm:"column(owner_id)" json:"owner_id"`
Name string `orm:"column(name)" json:"name"` Name string `orm:"column(name)" json:"name" sort:"default"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
Deleted bool `orm:"column(deleted)" json:"deleted"` Deleted bool `orm:"column(deleted)" json:"deleted"`

View File

@ -34,7 +34,7 @@ type RepoRecord struct {
Description string `orm:"column(description)" json:"description"` Description string `orm:"column(description)" json:"description"`
PullCount int64 `orm:"column(pull_count)" json:"pull_count"` PullCount int64 `orm:"column(pull_count)" json:"pull_count"`
StarCount int64 `orm:"column(star_count)" json:"star_count"` StarCount int64 `orm:"column(star_count)" json:"star_count"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time" sort:"default:desc"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
} }

View File

@ -24,7 +24,7 @@ const UserTable = "harbor_user"
// User holds the details of a user. // User holds the details of a user.
type User struct { type User struct {
UserID int `orm:"pk;auto;column(user_id)" json:"user_id"` UserID int `orm:"pk;auto;column(user_id)" json:"user_id"`
Username string `orm:"column(username)" json:"username"` Username string `orm:"column(username)" json:"username" sort:"default"`
Email string `orm:"column(email)" json:"email"` Email string `orm:"column(email)" json:"email"`
Password string `orm:"column(password)" json:"password"` Password string `orm:"column(password)" json:"password"`
PasswordVersion string `orm:"column(password_version)" json:"password_version"` PasswordVersion string `orm:"column(password_version)" json:"password_version"`

View File

@ -178,7 +178,7 @@ func (t *RegistryAPI) List() {
queryStr = fmt.Sprintf("name=~%s", name) queryStr = fmt.Sprintf("name=~%s", name)
} }
} }
query, err := q.Build(queryStr, 0, 0) query, err := q.Build(queryStr, "", 0, 0)
if err != nil { if err != nil {
t.SendError(err) t.SendError(err)
return return

View File

@ -51,8 +51,6 @@ func (s *SearchAPI) Get() {
keyword := s.GetString("q") keyword := s.GetString("q")
query := q.New(q.KeyWords{}) query := q.New(q.KeyWords{})
query.Sorting = "name"
if keyword != "" { if keyword != "" {
query.Keywords["name"] = &q.FuzzyMatchValue{Value: keyword} query.Keywords["name"] = &q.FuzzyMatchValue{Value: keyword}
} }

View File

@ -442,7 +442,9 @@ func (gc *GarbageCollector) removeUntaggedBlobs(ctx job.Context) {
}, },
PageNumber: 1, PageNumber: 1,
PageSize: int64(ps), PageSize: int64(ps),
Sorting: "id", Sorts: []*q.Sort{
q.NewSort("id", false),
},
} }
blobs, err := gc.blobMgr.List(ctx.SystemContext(), q) blobs, err := gc.blobMgr.List(ctx.SystemContext(), q)
if err != nil { if err != nil {

217
src/lib/orm/metadata.go Normal file
View File

@ -0,0 +1,217 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package orm
import (
"context"
"reflect"
"strings"
"sync"
"unicode"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/lib/q"
)
var (
// cache the parsed models
cache = sync.Map{}
)
type key struct {
Name string
Filterable bool
FilterFunc func(context.Context, orm.QuerySeter, string, interface{}) orm.QuerySeter
Sortable bool
}
type metadata struct {
Keys map[string]*key
DefaultSort *q.Sort
}
func (m *metadata) Filterable(key string) (*key, bool) {
k, exist := m.Keys[key]
return k, exist
}
func (m *metadata) Sortable(key string) bool {
k, exist := m.Keys[key]
if !exist {
return false
}
return k.Sortable
}
// parse the definition of the provided model(fields/methods/annotations) and return the parsed metadata
func parseModel(model interface{}) *metadata {
// pointer type
ptr := reflect.TypeOf(model)
// struct type
t := ptr.Elem()
// get the metadata from cache first
fullName := getFullName(t)
cacheMetadata, exist := cache.Load(fullName)
if exist {
return cacheMetadata.(*metadata)
}
// pointer value
v := reflect.ValueOf(model)
metadata := &metadata{
Keys: map[string]*key{},
}
// parse fields of the provided model
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
orm := field.Tag.Get("orm")
// isn't the database column, skip
if orm == "-" {
continue
}
filterable := parseFilterable(field)
defaultSort, sortable := parseSortable(field)
column := parseColumn(field)
metadata.Keys[field.Name] = &key{
Name: field.Name,
Filterable: filterable,
Sortable: sortable,
}
metadata.Keys[column] = &key{
Name: column,
Filterable: filterable,
Sortable: sortable,
}
if defaultSort != nil {
metadata.DefaultSort = defaultSort
}
}
// parse methods of the provided model
for i := 0; i < ptr.NumMethod(); i++ {
methodName := ptr.Method(i).Name
if !strings.HasPrefix(methodName, "FilterBy") {
continue
}
methodValue := v.MethodByName(methodName)
if !methodValue.IsValid() {
continue
}
filterFunc, ok := methodValue.Interface().(func(context.Context, orm.QuerySeter, string, interface{}) orm.QuerySeter)
if !ok {
continue
}
field := strings.TrimPrefix(methodName, "FilterBy")
metadata.Keys[field] = &key{
Name: field,
Filterable: true,
FilterFunc: filterFunc,
}
snakeCaseField := snakeCase(field)
metadata.Keys[snakeCaseField] = &key{
Name: snakeCaseField,
Filterable: true,
FilterFunc: filterFunc,
}
}
cache.Store(fullName, metadata)
return metadata
}
// parseFilterable parses whether the field is filterable according to the field annotation
// For the following struct definition, "Field1" isn't filterable and "Field2" is filterable
// type Model struct {
// Field1 string `filter:"false"`
// Field2 string
// }
func parseFilterable(field reflect.StructField) bool {
return field.Tag.Get("filter") != "false"
}
// parseSortable parses whether the field is sortable according to the field annotation
// If the field is sortable and is also specified as the default sort, return a q.Sort model as well
// For the following struct definition, "Field1" isn't sortable and "Field2", "Field2", "Field4", "Field5" are all sortable
// type Model struct {
// Field1 string `sort:"false"`
// Field2 string `sort:"true;default"`
// Field3 string `sort:"true;default:desc"`
// Field4 string `sort:"default"`
// Field5 string
// }
func parseSortable(field reflect.StructField) (*q.Sort, bool) {
var defaultSort *q.Sort
for _, item := range strings.Split(field.Tag.Get("sort"), ";") {
// isn't sortable, return directly
if item == "false" {
return nil, false
}
if !strings.HasPrefix(item, "default") {
continue
}
defaultSort = &q.Sort{
Key: field.Name,
DESC: false,
}
if strings.TrimPrefix(item, "default") == ":desc" {
defaultSort.DESC = true
}
}
return defaultSort, true
}
// parseColumn parses the column name according to the field annotation
// type Model struct {
// Field1 string `orm:"column(customized_field1)"`
// Field2 string
// }
// It returns "customized_field1" for "Field1" and returns "field2" for "Field2"
func parseColumn(field reflect.StructField) string {
column := ""
for _, item := range strings.Split(field.Tag.Get("orm"), ";") {
if !strings.HasPrefix(item, "column") {
continue
}
item = strings.TrimPrefix(item, "column(")
item = strings.TrimSuffix(item, ")")
column = item
break
}
if len(column) == 0 {
column = snakeCase(field.Name)
}
return column
}
// convert string to snake case
func snakeCase(str string) string {
delim := '_'
runes := []rune(str)
var out []rune
for i := 0; i < len(runes); i++ {
if i > 0 &&
(unicode.IsUpper(runes[i])) &&
((i+1 < len(runes) && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) {
out = append(out, delim)
}
out = append(out, unicode.ToLower(runes[i]))
}
return string(out)
}

View File

@ -0,0 +1,123 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package orm
import (
"context"
"testing"
"github.com/astaxie/beego/orm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type foo struct {
Field1 string `orm:"-"`
Field2 string `orm:"column(customized_field2)" filter:"false"`
Field3 string `sort:"false"`
Field4 string `sort:"default:desc"`
}
func (f *foo) FilterByField5(context.Context, orm.QuerySeter, string, interface{}) orm.QuerySeter {
return nil
}
func (f *foo) OtherFunc() {}
func TestParseQueryObject(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
metadata := parseModel(&foo{})
require.NotNil(metadata)
require.Len(metadata.Keys, 8)
key, exist := metadata.Keys["Field2"]
require.True(exist)
assert.Equal("Field2", key.Name)
assert.False(key.Filterable)
assert.True(key.Sortable)
key, exist = metadata.Keys["customized_field2"]
require.True(exist)
assert.Equal("customized_field2", key.Name)
assert.False(key.Filterable)
assert.True(key.Sortable)
key, exist = metadata.Keys["Field3"]
require.True(exist)
assert.Equal("Field3", key.Name)
assert.True(key.Filterable)
assert.False(key.Sortable)
key, exist = metadata.Keys["field3"]
require.True(exist)
assert.Equal("field3", key.Name)
assert.True(key.Filterable)
assert.False(key.Sortable)
key, exist = metadata.Keys["Field4"]
require.True(exist)
assert.Equal("Field4", key.Name)
assert.True(key.Filterable)
assert.True(key.Sortable)
key, exist = metadata.Keys["field4"]
require.True(exist)
assert.Equal("field4", key.Name)
assert.True(key.Filterable)
assert.True(key.Sortable)
key, exist = metadata.Keys["Field5"]
require.True(exist)
assert.Equal("Field5", key.Name)
assert.True(key.Filterable)
assert.False(key.Sortable)
key, exist = metadata.Keys["field5"]
require.True(exist)
assert.Equal("field5", key.Name)
assert.True(key.Filterable)
assert.False(key.Sortable)
require.NotNil(metadata.DefaultSort)
assert.Equal("Field4", metadata.DefaultSort.Key)
assert.True(metadata.DefaultSort.DESC)
}
func Test_snakeCase(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want string
}{
{"ProjectID", args{"ProjectID"}, "project_id"},
{"project_id", args{"project_id"}, "project_id"},
{"RepositoryName", args{"RepositoryName"}, "repository_name"},
{"repository_name", args{"repository_name"}, "repository_name"},
{"ProfileURL", args{"ProfileURL"}, "profile_url"},
{"City", args{"City"}, "city"},
{"Address1", args{"Address1"}, "address1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := snakeCase(tt.args.str); got != tt.want {
t.Errorf("snakeCase() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -18,6 +18,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/goharbor/harbor/src/common/dao"
"strconv" "strconv"
"strings" "strings"
@ -25,6 +26,21 @@ import (
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
) )
// NewCondition alias function of orm.NewCondition
var NewCondition = orm.NewCondition
// Condition alias to orm.Condition
type Condition = orm.Condition
// Params alias to orm.Params
type Params = orm.Params
// ParamsList alias to orm.ParamsList
type ParamsList = orm.ParamsList
// QuerySeter alias to orm.QuerySeter
type QuerySeter = orm.QuerySeter
// RegisterModel ... // RegisterModel ...
func RegisterModel(models ...interface{}) { func RegisterModel(models ...interface{}) {
orm.RegisterModel(models...) orm.RegisterModel(models...)
@ -174,3 +190,18 @@ func CreateInClause(ctx context.Context, sql string, args ...interface{}) (strin
// when concat the in clause directly // when concat the in clause directly
return fmt.Sprintf(`IN (%s)`, strings.Join(idStrs, ",")), nil return fmt.Sprintf(`IN (%s)`, strings.Join(idStrs, ",")), nil
} }
// Escape special characters
func Escape(str string) string {
return dao.Escape(str)
}
// ParamPlaceholderForIn returns a string that contains placeholders for sql keyword "in"
// e.g. n=3, returns "?,?,?"
func ParamPlaceholderForIn(n int) string {
placeholders := []string{}
for i := 0; i < n; i++ {
placeholders = append(placeholders, "?")
}
return strings.Join(placeholders, ",")
}

View File

@ -16,68 +16,33 @@ package orm
import ( import (
"context" "context"
"fmt"
"reflect" "reflect"
"strings" "strings"
"sync"
"unicode"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/lib/q"
) )
// NewCondition alias function of orm.NewCondition // QuerySetter generates the query setter according to the provided model and query.
var NewCondition = orm.NewCondition // e.g.
// Condition alias to orm.Condition
type Condition = orm.Condition
// Params alias to orm.Params
type Params = orm.Params
// ParamsList alias to orm.ParamsList
type ParamsList = orm.ParamsList
// QuerySeter alias to orm.QuerySeter
type QuerySeter = orm.QuerySeter
// Escape special characters
func Escape(str string) string {
return dao.Escape(str)
}
// ParamPlaceholderForIn returns a string that contains placeholders for sql keyword "in"
// e.g. n=3, returns "?,?,?"
func ParamPlaceholderForIn(n int) string {
placeholders := []string{}
for i := 0; i < n; i++ {
placeholders = append(placeholders, "?")
}
return strings.Join(placeholders, ",")
}
// QuerySetter generates the query setter according to the query. "ignoredCols" is used to set the columns that will not be queried.
// Currently, it supports two ways to generate the query setter, the first one is to generate by the fields of the model,
// and the second one is to generate by the methods their name begins with `FilterBy` of the model.
// e.g. for the following model the queriable fields are :
// "Field2", "customized_field2", "Field3", "field3" and "Field4" (or "field4").
// type Foo struct{ // type Foo struct{
// Field1 string `orm:"-"` // Field1 string `orm:"-"` // can not filter/sort
// Field2 string `orm:"column(customized_field2)"` // Field2 string `orm:"column(customized_field2)"` // support filter by "Field2", "customized_field2"
// Field3 string // Field3 string `sort:"false"` // cannot be sorted
// Field4 string `sort:"default:desc"` // the default field/order(asc/desc) to sort if no sorting specified in query.
// Field5 string `filter:"false"` // cannot be filtered
// } // }
// // // support filter by "Field6", "field6"
// func (f *Foo) FilterByField4(ctx context.Context, qs orm.QuerySeter, key string, value interface{}) orm.QuerySeter { // func (f *Foo) FilterByField6(ctx context.Context, qs orm.QuerySetter, key string, value interface{}) orm.QuerySetter {
// // The value is the raw value of key in q.Query // ...
// return qs // return qs
// } // }
func QuerySetter(ctx context.Context, model interface{}, query *q.Query, ignoredCols ...string) (orm.QuerySeter, error) { func QuerySetter(ctx context.Context, model interface{}, query *q.Query) (orm.QuerySeter, error) {
val := reflect.ValueOf(model) t := reflect.TypeOf(model)
if val.Kind() != reflect.Ptr { if t.Kind() != reflect.Ptr {
return nil, errors.Errorf("<orm.QuerySetter> cannot use non-ptr model struct `%s`", getFullName(reflect.Indirect(val).Type())) return nil, fmt.Errorf("<orm.QuerySetter> cannot use non-ptr model struct `%s`", getFullName(t.Elem()))
} }
ormer, err := FromContext(ctx) ormer, err := FromContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -87,190 +52,116 @@ func QuerySetter(ctx context.Context, model interface{}, query *q.Query, ignored
return qs, nil return qs, nil
} }
ignored := map[string]bool{} metadata := parseModel(model)
for _, col := range ignoredCols { // set filters
ignored[col] = true qs = setFilters(ctx, qs, query, metadata)
}
columns := queriableColumns(model) // sorting
methods := queriableMethods(model) qs = setSorts(qs, query, metadata)
for k, v := range query.Keywords {
field := strings.SplitN(k, orm.ExprSep, 2)[0]
if ignored[field] {
continue
}
if columns[field] {
qs = queryByColumn(qs, k, v)
} else if method, ok := methods[snakeCase(field)]; ok {
qs = queryByMethod(ctx, qs, k, v, method, val)
}
}
// pagination
if query.PageSize > 0 { if query.PageSize > 0 {
qs = qs.Limit(query.PageSize) qs = qs.Limit(query.PageSize)
if query.PageNumber > 0 { if query.PageNumber > 0 {
qs = qs.Offset(query.PageSize * (query.PageNumber - 1)) qs = qs.Offset(query.PageSize * (query.PageNumber - 1))
} }
} }
return qs, nil return qs, nil
} }
// QuerySetterForCount creates the query setter used for count with the sort and pagination information ignored
func QuerySetterForCount(ctx context.Context, model interface{}, query *q.Query, ignoredCols ...string) (orm.QuerySeter, error) {
query = q.MustClone(query)
query.Sorts = nil
query.PageSize = 0
query.PageNumber = 0
return QuerySetter(ctx, model, query)
}
// set filters according to the query
func setFilters(ctx context.Context, qs orm.QuerySeter, query *q.Query, meta *metadata) orm.QuerySeter {
for key, value := range query.Keywords {
// The "strings.SplitN()" here is a workaround for the incorrect usage of query which should be avoided
// e.g. use the query with the knowledge of underlying ORM implementation, the "OrList" should be used instead:
// https://github.com/goharbor/harbor/blob/v2.2.0/src/controller/project/controller.go#L348
k := strings.SplitN(key, orm.ExprSep, 2)[0]
mk, filterable := meta.Filterable(k)
if !filterable {
// This is a workaround for the unsuitable usage of query, the keyword format for field and method should be consistent
// e.g. "ArtifactDigest" or the snake case format "artifact_digest" should be used instead:
// https://github.com/goharbor/harbor/blob/v2.2.0/src/controller/blob/controller.go#L233
mk, filterable = meta.Filterable(snakeCase(k))
if !filterable {
continue
}
}
// filter function defined, use it directly
if mk.FilterFunc != nil {
qs = mk.FilterFunc(ctx, qs, key, value)
continue
}
// fuzzy match
if f, ok := value.(*q.FuzzyMatchValue); ok {
qs = qs.Filter(key+"__icontains", Escape(f.Value))
continue
}
// range
if r, ok := value.(*q.Range); ok {
if r.Min != nil {
qs = qs.Filter(key+"__gte", r.Min)
}
if r.Max != nil {
qs = qs.Filter(key+"__lte", r.Max)
}
continue
}
// or list
if ol, ok := value.(*q.OrList); ok {
if len(ol.Values) > 0 {
qs = qs.Filter(key+"__in", ol.Values...)
}
continue
}
// and list
if _, ok := value.(*q.AndList); ok {
// do nothing as and list needs to be handled by the logic of DAO
continue
}
// exact match
qs = qs.Filter(key, value)
}
return qs
}
// set sorts according to the query
func setSorts(qs orm.QuerySeter, query *q.Query, meta *metadata) orm.QuerySeter {
var sortings []string
for _, sort := range query.Sorts {
if !meta.Sortable(sort.Key) {
continue
}
sorting := sort.Key
if sort.DESC {
sorting = fmt.Sprintf("-%s", sorting)
}
sortings = append(sortings, sorting)
}
// if no sorts are specified, apply the default sort setting if exists
if len(sortings) == 0 && meta.DefaultSort != nil {
sorting := meta.DefaultSort.Key
if meta.DefaultSort.DESC {
sorting = fmt.Sprintf("-%s", sorting)
}
sortings = append(sortings, sorting)
}
if len(sortings) > 0 {
qs = qs.OrderBy(sortings...)
}
return qs
}
// get reflect.Type name with package path. // get reflect.Type name with package path.
func getFullName(typ reflect.Type) string { func getFullName(typ reflect.Type) string {
return typ.PkgPath() + "." + typ.Name() return typ.PkgPath() + "." + typ.Name()
} }
// convert string to snake case
func snakeCase(str string) string {
delim := '_'
runes := []rune(str)
var out []rune
for i := 0; i < len(runes); i++ {
if i > 0 &&
(unicode.IsUpper(runes[i])) &&
((i+1 < len(runes) && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) {
out = append(out, delim)
}
out = append(out, unicode.ToLower(runes[i]))
}
return string(out)
}
func queryByColumn(qs orm.QuerySeter, key string, value interface{}) orm.QuerySeter {
// fuzzy match
if f, ok := value.(*q.FuzzyMatchValue); ok {
return qs.Filter(key+"__icontains", Escape(f.Value))
}
// range
if r, ok := value.(*q.Range); ok {
if r.Min != nil {
qs = qs.Filter(key+"__gte", r.Min)
}
if r.Max != nil {
qs = qs.Filter(key+"__lte", r.Max)
}
return qs
}
// or list
if ol, ok := value.(*q.OrList); ok {
if len(ol.Values) > 0 {
qs = qs.Filter(key+"__in", ol.Values...)
}
return qs
}
// and list
if _, ok := value.(*q.AndList); ok {
// do nothing as and list needs to be handled by the logic of DAO
return qs
}
// exact match
return qs.Filter(key, value)
}
func queryByMethod(ctx context.Context, qs orm.QuerySeter, key string, value interface{}, methodName string, reflectVal reflect.Value) orm.QuerySeter {
if mv := reflectVal.MethodByName(methodName); mv.IsValid() {
switch method := mv.Interface().(type) {
case func(context.Context, orm.QuerySeter, string, interface{}) orm.QuerySeter:
return method(ctx, qs, key, value)
default:
return qs
}
}
return qs
}
var (
cache = sync.Map{}
)
// get model fields which are columns in orm
// e.g. for the following model the columns that can be queried are:
// "Field2", "customized_field2", "Field3" and "field3"
// type model struct{
// Field1 string `orm:"-"`
// Field2 string `orm:"column(customized_field2)"`
// Field3 string
// }
func queriableColumns(model interface{}) map[string]bool {
typ := reflect.Indirect(reflect.ValueOf(model)).Type()
key := getFullName(typ) + "-columns"
value, ok := cache.Load(key)
if ok {
return value.(map[string]bool)
}
cols := map[string]bool{}
defer func() {
cache.Store(key, cols)
}()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
orm := field.Tag.Get("orm")
if orm == "-" {
continue
}
colName := ""
for _, str := range strings.Split(orm, ";") {
if strings.HasPrefix(str, "column") {
str = strings.TrimPrefix(str, "column(")
str = strings.TrimSuffix(str, ")")
if len(str) > 0 {
colName = str
break
}
}
}
if colName == "" {
colName = snakeCase(field.Name)
}
cols[colName] = true
cols[field.Name] = true
}
return cols
}
// get model methods which begin with `FilterBy`
func queriableMethods(model interface{}) map[string]string {
val := reflect.ValueOf(model)
key := getFullName(reflect.Indirect(val).Type()) + "-methods"
value, ok := cache.Load(key)
if ok {
return value.(map[string]string)
}
methods := map[string]string{}
defer func() {
cache.Store(key, methods)
}()
prefix := "FilterBy"
typ := val.Type()
for i := 0; i < typ.NumMethod(); i++ {
name := typ.Method(i).Name
if !strings.HasPrefix(name, prefix) {
continue
}
field := snakeCase(strings.TrimPrefix(name, prefix))
if field != "" {
methods[field] = name
}
}
return methods
}

View File

@ -1,109 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package orm
import (
"reflect"
"testing"
)
func Test_snakeCase(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want string
}{
{"ProjectID", args{"ProjectID"}, "project_id"},
{"project_id", args{"project_id"}, "project_id"},
{"RepositoryName", args{"RepositoryName"}, "repository_name"},
{"repository_name", args{"repository_name"}, "repository_name"},
{"ProfileURL", args{"ProfileURL"}, "profile_url"},
{"City", args{"City"}, "city"},
{"Address1", args{"Address1"}, "address1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := snakeCase(tt.args.str); got != tt.want {
t.Errorf("snakeCase() = %v, want %v", got, tt.want)
}
})
}
}
type Bar struct {
Field1 string `orm:"-"`
Field2 string `orm:"column(customized_field2)"`
Field3 string
FirstName string
}
func (Bar) Foo() {}
func (bar *Bar) FilterBy() {}
func (bar *Bar) FilterByField4() {}
func Test_queriableColumns(t *testing.T) {
toWant := func(fields ...string) map[string]bool {
want := map[string]bool{}
for _, field := range fields {
want[field] = true
}
return want
}
type args struct {
model interface{}
}
tests := []struct {
name string
args args
want map[string]bool
}{
{"bar", args{&Bar{}}, toWant("Field2", "customized_field2", "Field3", "field3", "FirstName", "first_name")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := queriableColumns(tt.args.model); !reflect.DeepEqual(got, tt.want) {
t.Errorf("queriableColumns() = %v, want %v", got, tt.want)
}
})
}
}
func Test_queriableMethods(t *testing.T) {
type args struct {
model interface{}
}
tests := []struct {
name string
args args
want map[string]string
}{
{"bar", args{&Bar{}}, map[string]string{"field4": "FilterByField4"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := queriableMethods(tt.args.model); !reflect.DeepEqual(got, tt.want) {
t.Errorf("queriableMethods() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -16,29 +16,41 @@ package q
import ( import (
"fmt" "fmt"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
) )
// Build query sting and pagination information into the Query model // Build query sting, sort and pagination information into the Query model
// query string format: q=k=v,k=~v,k=[min~max],k={v1 v2 v3},k=(v1 v2 v3) // query string format: q=k=v,k=~v,k=[min~max],k={v1 v2 v3},k=(v1 v2 v3)
// exact match: k=v // exact match: k=v
// fuzzy match: k=~v // fuzzy match: k=~v
// range: k=[min~max] // range: k=[min~max]
// or list: k={v1 v2 v3} // or list: k={v1 v2 v3}
// and list: k=(v1 v2 v3) // and list: k=(v1 v2 v3)
func Build(q string, pageNumber, pageSize int64) (*Query, error) { // sort format: sort=k1,-k2
query := &Query{ func Build(q, sort string, pageNumber, pageSize int64) (*Query, error) {
keywords, err := parseKeywords(q)
if err != nil {
return nil, err
}
sorts := parseSorting(sort)
return &Query{
Keywords: keywords,
Sorts: sorts,
PageNumber: pageNumber, PageNumber: pageNumber,
PageSize: pageSize, PageSize: pageSize,
Keywords: map[string]interface{}{}, }, nil
} }
func parseKeywords(q string) (map[string]interface{}, error) {
keywords := map[string]interface{}{}
if len(q) == 0 { if len(q) == 0 {
return query, nil return keywords, nil
} }
// try to escaped the 'q=tags%3Dnil' when to filter tags. // try to escaped the 'q=tags%3Dnil' when to filter tags.
if unescapedQuery, err := url.QueryUnescape(q); err == nil { if unescapedQuery, err := url.QueryUnescape(q); err == nil {
@ -60,9 +72,26 @@ func Build(q string, pageNumber, pageSize int64) (*Query, error) {
WithCode(errors.BadRequestCode). WithCode(errors.BadRequestCode).
WithMessage("invalid query string value: %s", strs[1]) WithMessage("invalid query string value: %s", strs[1])
} }
query.Keywords[strs[0]] = value keywords[strs[0]] = value
} }
return query, nil return keywords, nil
}
func parseSorting(sort string) []*Sort {
var sorts []*Sort
for _, sorting := range strings.Split(sort, ",") {
key := sorting
desc := false
if strings.HasPrefix(sorting, "-") {
key = strings.TrimPrefix(sorting, "-")
desc = true
}
sorts = append(sorts, &Sort{
Key: key,
DESC: desc,
})
}
return sorts
} }
func parsePattern(value string) (interface{}, error) { func parsePattern(value string) (interface{}, error) {

View File

@ -241,32 +241,26 @@ func TestParsePattern(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
} }
func TestBuild(t *testing.T) { func TestParseKeywords(t *testing.T) {
// empty string // empty string
q := `` q := ``
query, err := Build(q, 1, 10) keywords, err := parseKeywords(q)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, query) require.NotNil(t, keywords)
assert.Equal(t, int64(1), query.PageNumber)
assert.Equal(t, int64(10), query.PageSize)
// contains only "," // contains only ","
q = `,` q = `,`
query, err = Build(q, 1, 10) keywords, err = parseKeywords(q)
require.NotNil(t, err) require.NotNil(t, err)
// valid query string // valid query string
q = `k=v` q = `k=v`
query, err = Build(q, 1, 10) keywords, err = parseKeywords(q)
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, int64(1), query.PageNumber) assert.Equal(t, "v", keywords["k"].(string))
assert.Equal(t, int64(10), query.PageSize)
assert.Equal(t, "v", query.Keywords["k"].(string))
q = `q=tags%3Dnil` q = `q=tags%3Dnil`
query, err = Build(q, 1, 10) keywords, err = parseKeywords(q)
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, int64(1), query.PageNumber) assert.Equal(t, "tags=nil", keywords["q"].(string))
assert.Equal(t, int64(10), query.PageSize)
assert.Equal(t, "tags=nil", query.Keywords["q"].(string))
} }

View File

@ -19,22 +19,24 @@ type KeyWords = map[string]interface{}
// Query parameters // Query parameters
type Query struct { type Query struct {
// Filter list
Keywords KeyWords
// Sort list
Sorts []*Sort
// Page number // Page number
PageNumber int64 PageNumber int64
// Page size // Page size
PageSize int64 PageSize int64
// List of key words // Deprecate, use "Sorts" instead
Keywords KeyWords
// Sorting
Sorting string Sorting string
} }
// First make the query only fetch the first one record in the sorting order // First make the query only fetch the first one record in the sorting order
func (q *Query) First(sorting ...string) *Query { func (q *Query) First(sorting ...*Sort) *Query {
q.PageNumber = 1 q.PageNumber = 1
q.PageSize = 1 q.PageSize = 1
if len(sorting) > 0 { if len(sorting) > 0 {
q.Sorting = sorting[0] q.Sorts = append(q.Sorts, sorting...)
} }
return q return q
@ -54,14 +56,26 @@ func MustClone(query *Query) *Query {
if query != nil { if query != nil {
q.PageNumber = query.PageNumber q.PageNumber = query.PageNumber
q.PageSize = query.PageSize q.PageSize = query.PageSize
q.Sorting = query.Sorting q.Sorts = query.Sorts
for k, v := range query.Keywords { for k, v := range query.Keywords {
q.Keywords[k] = v q.Keywords[k] = v
} }
for _, sort := range query.Sorts {
q.Sorts = append(q.Sorts, &Sort{
Key: sort.Key,
DESC: sort.DESC,
})
}
} }
return q return q
} }
// Sort specifies the order information
type Sort struct {
Key string
DESC bool
}
// Range query // Range query
type Range struct { type Range struct {
Min interface{} Min interface{}
@ -82,3 +96,40 @@ type OrList struct {
type FuzzyMatchValue struct { type FuzzyMatchValue struct {
Value string Value string
} }
// NewSort creates new sort
func NewSort(key string, desc bool) *Sort {
return &Sort{
Key: key,
DESC: desc,
}
}
// NewRange creates a new range
func NewRange(min, max interface{}) *Range {
return &Range{
Min: min,
Max: max,
}
}
// NewAndList creates a new and list
func NewAndList(values ...interface{}) *AndList {
return &AndList{
Values: values,
}
}
// NewOrList creates a new or list
func NewOrList(values ...interface{}) *OrList {
return &OrList{
Values: values,
}
}
// NewFuzzyMatchValue creates a new fuzzy match
func NewFuzzyMatchValue(value string) *FuzzyMatchValue {
return &FuzzyMatchValue{
Value: value,
}
}

View File

@ -97,7 +97,6 @@ func (d *dao) List(ctx context.Context, query *q.Query) ([]*Artifact, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
qs = qs.OrderBy("-PushTime", "ID")
if _, err = qs.All(&artifacts); err != nil { if _, err = qs.All(&artifacts); err != nil {
return nil, err return nil, err
} }

View File

@ -37,7 +37,7 @@ type Artifact struct {
Digest string `orm:"column(digest)"` Digest string `orm:"column(digest)"`
Size int64 `orm:"column(size)"` Size int64 `orm:"column(size)"`
Icon string `orm:"column(icon)"` Icon string `orm:"column(icon)"`
PushTime time.Time `orm:"column(push_time)"` PushTime time.Time `orm:"column(push_time)" sort:"default:desc"`
PullTime time.Time `orm:"column(pull_time)"` PullTime time.Time `orm:"column(pull_time)"`
ExtraAttrs string `orm:"column(extra_attrs)"` // json string ExtraAttrs string `orm:"column(extra_attrs)"` // json string
Annotations string `orm:"column(annotations);type(jsonb)"` // json string Annotations string `orm:"column(annotations);type(jsonb)"` // json string

View File

@ -45,13 +45,7 @@ type dao struct{}
// Count ... // Count ...
func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) { func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) {
if query != nil { qs, err := orm.QuerySetterForCount(ctx, &model.AuditLog{}, query)
// ignore the page number and size
query = &q.Query{
Keywords: query.Keywords,
}
}
qs, err := orm.QuerySetter(ctx, &model.AuditLog{}, query)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -65,7 +59,6 @@ func (d *dao) List(ctx context.Context, query *q.Query) ([]*model.AuditLog, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
qs = qs.OrderBy("-op_time")
if _, err = qs.All(&audit); err != nil { if _, err = qs.All(&audit); err != nil {
return nil, err return nil, err
} }

View File

@ -17,7 +17,7 @@ type AuditLog struct {
ResourceType string `orm:"column(resource_type)" json:"resource_type"` ResourceType string `orm:"column(resource_type)" json:"resource_type"`
Resource string `orm:"column(resource)" json:"resource"` Resource string `orm:"column(resource)" json:"resource"`
Username string `orm:"column(username)" json:"username"` Username string `orm:"column(username)" json:"username"`
OpTime time.Time `orm:"column(op_time)" json:"op_time"` OpTime time.Time `orm:"column(op_time)" json:"op_time" sort:"default:desc"`
} }
// TableName for audit log // TableName for audit log

View File

@ -222,10 +222,6 @@ func (d *dao) ListBlobs(ctx context.Context, query *q.Query) ([]*models.Blob, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
if query.Sorting != "" {
qs = qs.OrderBy(query.Sorting)
}
blobs := []*models.Blob{} blobs := []*models.Blob{}
if _, err = qs.All(&blobs); err != nil { if _, err = qs.All(&blobs); err != nil {
return nil, err return nil, err

View File

@ -100,9 +100,6 @@ func (i *iDao) ListImmutableRules(ctx context.Context, query *q.Query) ([]*model
if err != nil { if err != nil {
return nil, err return nil, err
} }
if query.Sorting != "" {
qs = qs.OrderBy(query.Sorting)
}
if _, err = qs.All(&rules); err != nil { if _, err = qs.All(&rules); err != nil {
return nil, err return nil, err
} }
@ -111,12 +108,7 @@ func (i *iDao) ListImmutableRules(ctx context.Context, query *q.Query) ([]*model
// Count ... // Count ...
func (i *iDao) Count(ctx context.Context, query *q.Query) (int64, error) { func (i *iDao) Count(ctx context.Context, query *q.Query) (int64, error) {
query = q.MustClone(query) qs, err := orm.QuerySetterForCount(ctx, &model.ImmutableRule{}, query)
query.Sorting = ""
query.PageNumber = 0
query.PageSize = 0
qs, err := orm.QuerySetter(ctx, &model.ImmutableRule{}, query)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -123,13 +123,7 @@ func (d *dao) Delete(ctx context.Context, id int64) error {
// List count instances by query params. // List count instances by query params.
func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error) { func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error) {
if query != nil { qs, err := orm.QuerySetterForCount(ctx, &provider.Instance{}, query)
// ignore the page number and size
query = &q.Query{
Keywords: query.Keywords,
}
}
qs, err := orm.QuerySetter(ctx, &provider.Instance{}, query)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -51,14 +51,7 @@ type dao struct{}
// Count returns the total count of policies according to the query // Count returns the total count of policies according to the query
func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error) { func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error) {
if query != nil { qs, err := orm.QuerySetterForCount(ctx, &policy.Schema{}, query)
// ignore the page number and size
query = &q.Query{
Keywords: query.Keywords,
}
}
qs, err := orm.QuerySetter(ctx, &policy.Schema{}, query)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -172,11 +165,8 @@ func (d *dao) List(ctx context.Context, query *q.Query) (schemas []*policy.Schem
if err != nil { if err != nil {
return return
} }
qs = qs.OrderBy("UpdatedTime", "ID")
if _, err = qs.All(&schemas); err != nil { if _, err = qs.All(&schemas); err != nil {
return return
} }
return schemas, nil return schemas, nil
} }

View File

@ -73,7 +73,7 @@ type Schema struct {
TriggerStr string `orm:"column(trigger)" json:"-"` TriggerStr string `orm:"column(trigger)" json:"-"`
Enabled bool `orm:"column(enabled)" json:"enabled"` Enabled bool `orm:"column(enabled)" json:"enabled"`
CreatedAt time.Time `orm:"column(creation_time)" json:"creation_time"` CreatedAt time.Time `orm:"column(creation_time)" json:"creation_time"`
UpdatedTime time.Time `orm:"column(update_time)" json:"update_time"` UpdatedTime time.Time `orm:"column(update_time)" json:"update_time" sort:"default"`
} }
// TableName specifies the policy schema table name. // TableName specifies the policy schema table name.

View File

@ -96,11 +96,7 @@ func (d *dao) Create(ctx context.Context, project *models.Project) (int64, error
func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error) { func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error) {
query = q.MustClone(query) query = q.MustClone(query)
query.Keywords["deleted"] = false query.Keywords["deleted"] = false
query.Sorting = "" qs, err := orm.QuerySetterForCount(ctx, &models.Project{}, query)
query.PageNumber = 0
query.PageSize = 0
qs, err := orm.QuerySetter(ctx, &models.Project{}, query)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -164,10 +160,6 @@ func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.Project, erro
return nil, err return nil, err
} }
if query.Sorting != "" {
qs = qs.OrderBy(query.Sorting)
}
projects := []*models.Project{} projects := []*models.Project{}
if _, err := qs.All(&projects); err != nil { if _, err := qs.All(&projects); err != nil {
return nil, err return nil, err

View File

@ -17,12 +17,13 @@ package dao
import ( import (
"context" "context"
"fmt" "fmt"
"time"
o "github.com/astaxie/beego/orm" o "github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/lib/q"
"time"
) )
// DAO is the data access object interface for repository // DAO is the data access object interface for repository
@ -53,13 +54,7 @@ func New() DAO {
type dao struct{} type dao struct{}
func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) { func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) {
if query != nil { qs, err := orm.QuerySetterForCount(ctx, &models.RepoRecord{}, query)
// ignore the page number and size
query = &q.Query{
Keywords: query.Keywords,
}
}
qs, err := orm.QuerySetter(ctx, &models.RepoRecord{}, query)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -71,7 +66,6 @@ func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.RepoRecord, e
if err != nil { if err != nil {
return nil, err return nil, err
} }
qs = qs.OrderBy("-CreationTime", "RepositoryID")
if _, err = qs.All(&repositories); err != nil { if _, err = qs.All(&repositories); err != nil {
return nil, err return nil, err
} }

View File

@ -83,12 +83,7 @@ func (d *dao) Get(ctx context.Context, id int64) (*model.Robot, error) {
} }
func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) { func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) {
query = q.MustClone(query) qs, err := orm.QuerySetterForCount(ctx, &model.Robot{}, query)
query.Sorting = ""
query.PageNumber = 0
query.PageSize = 0
qs, err := orm.QuerySetter(ctx, &model.Robot{}, query)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -119,9 +114,6 @@ func (d *dao) List(ctx context.Context, query *q.Query) ([]*model.Robot, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if query.Sorting != "" {
qs = qs.OrderBy(query.Sorting)
}
if _, err = qs.All(&robots); err != nil { if _, err = qs.All(&robots); err != nil {
return nil, err return nil, err
} }

View File

@ -81,9 +81,5 @@ func (m *manager) Update(ctx context.Context, r *model.Robot, props ...string) e
// List ... // List ...
func (m *manager) List(ctx context.Context, query *q.Query) ([]*model.Robot, error) { func (m *manager) List(ctx context.Context, query *q.Query) ([]*model.Robot, error) {
query = q.MustClone(query)
if query.Sorting == "" {
query.Sorting = "name"
}
return m.dao.List(ctx, query) return m.dao.List(ctx, query)
} }

View File

@ -15,7 +15,7 @@ func init() {
// Robot holds the details of a robot. // Robot holds the details of a robot.
type Robot struct { type Robot struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"` ID int64 `orm:"pk;auto;column(id)" json:"id"`
Name string `orm:"column(name)" json:"name"` Name string `orm:"column(name)" json:"name" sort:"default"`
Description string `orm:"column(description)" json:"description"` Description string `orm:"column(description)" json:"description"`
Secret string `orm:"column(secret)" json:"secret"` Secret string `orm:"column(secret)" json:"secret"`
Salt string `orm:"column(salt)" json:"-"` Salt string `orm:"column(salt)" json:"-"`

View File

@ -28,12 +28,7 @@ func init() {
// GetTotalOfRegistrations returns the total count of scanner registrations according to the query. // GetTotalOfRegistrations returns the total count of scanner registrations according to the query.
func GetTotalOfRegistrations(ctx context.Context, query *q.Query) (int64, error) { func GetTotalOfRegistrations(ctx context.Context, query *q.Query) (int64, error) {
query = q.MustClone(query) qs, err := orm.QuerySetterForCount(ctx, &Registration{}, query)
query.Sorting = ""
query.PageNumber = 0
query.PageSize = 0
qs, err := orm.QuerySetter(ctx, &Registration{}, query)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -53,13 +53,7 @@ func New() DAO {
type dao struct{} type dao struct{}
func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) { func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) {
if query != nil { qs, err := orm.QuerySetterForCount(ctx, &tag.Tag{}, query)
// ignore the page number and size
query = &q.Query{
Keywords: query.Keywords,
}
}
qs, err := orm.QuerySetter(ctx, &tag.Tag{}, query)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -71,7 +65,6 @@ func (d *dao) List(ctx context.Context, query *q.Query) ([]*tag.Tag, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
qs = qs.OrderBy("-PushTime", "ID")
if _, err = qs.All(&tags); err != nil { if _, err = qs.All(&tags); err != nil {
return nil, err return nil, err
} }

View File

@ -24,6 +24,6 @@ type Tag struct {
RepositoryID int64 `orm:"column(repository_id)" json:"repository_id"` // tags are the resources of repository, one repository only contains one same name tag RepositoryID int64 `orm:"column(repository_id)" json:"repository_id"` // tags are the resources of repository, one repository only contains one same name tag
ArtifactID int64 `orm:"column(artifact_id)" json:"artifact_id"` // the artifact ID that the tag attaches to, it changes when pushing a same name but different digest artifact ArtifactID int64 `orm:"column(artifact_id)" json:"artifact_id"` // the artifact ID that the tag attaches to, it changes when pushing a same name but different digest artifact
Name string `orm:"column(name)" json:"name"` Name string `orm:"column(name)" json:"name"`
PushTime time.Time `orm:"column(push_time)" json:"push_time"` PushTime time.Time `orm:"column(push_time)" json:"push_time" sort:"default:desc"`
PullTime time.Time `orm:"column(pull_time)" json:"pull_time"` PullTime time.Time `orm:"column(pull_time)" json:"pull_time"`
} }

View File

@ -83,7 +83,6 @@ func (e *executionDAO) List(ctx context.Context, query *q.Query) ([]*Execution,
if err != nil { if err != nil {
return nil, err return nil, err
} }
qs = qs.OrderBy("-StartTime")
if _, err = qs.All(&executions); err != nil { if _, err = qs.All(&executions); err != nil {
return nil, err return nil, err
} }

View File

@ -37,7 +37,7 @@ type Execution struct {
StatusMessage string `orm:"column(status_message)"` StatusMessage string `orm:"column(status_message)"`
Trigger string `orm:"column(trigger)"` Trigger string `orm:"column(trigger)"`
ExtraAttrs string `orm:"column(extra_attrs)"` // json string ExtraAttrs string `orm:"column(extra_attrs)"` // json string
StartTime time.Time `orm:"column(start_time)"` StartTime time.Time `orm:"column(start_time)" sort:"default:desc"`
UpdateTime time.Time `orm:"column(update_time)"` UpdateTime time.Time `orm:"column(update_time)"`
EndTime time.Time `orm:"column(end_time)"` EndTime time.Time `orm:"column(end_time)"`
Revision int64 `orm:"column(revision)"` Revision int64 `orm:"column(revision)"`
@ -67,7 +67,7 @@ type Task struct {
RunCount int32 `orm:"column(run_count)"` RunCount int32 `orm:"column(run_count)"`
ExtraAttrs string `orm:"column(extra_attrs)"` // json string ExtraAttrs string `orm:"column(extra_attrs)"` // json string
CreationTime time.Time `orm:"column(creation_time)"` CreationTime time.Time `orm:"column(creation_time)"`
StartTime time.Time `orm:"column(start_time)"` StartTime time.Time `orm:"column(start_time)" sort:"default:desc"`
UpdateTime time.Time `orm:"column(update_time)"` UpdateTime time.Time `orm:"column(update_time)"`
EndTime time.Time `orm:"column(end_time)"` EndTime time.Time `orm:"column(end_time)"`
} }

View File

@ -76,7 +76,6 @@ func (t *taskDAO) List(ctx context.Context, query *q.Query) ([]*Task, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
qs = qs.OrderBy("-StartTime")
if _, err = qs.All(&tasks); err != nil { if _, err = qs.All(&tasks); err != nil {
return nil, err return nil, err
} }

View File

@ -45,10 +45,6 @@ func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.User, error)
return nil, err return nil, err
} }
if query.Sorting != "" {
qs = qs.OrderBy(query.Sorting)
}
var users []*models.User var users []*models.User
if _, err := qs.All(&users); err != nil { if _, err := qs.All(&users); err != nil {
return nil, err return nil, err

View File

@ -82,10 +82,6 @@ func (m *manager) GetByName(ctx context.Context, username string) (*models.User,
// List users according to the query // List users according to the query
func (m *manager) List(ctx context.Context, query *q.Query) (models.Users, error) { func (m *manager) List(ctx context.Context, query *q.Query) (models.Users, error) {
query = q.MustClone(query) query = q.MustClone(query)
if query.Sorting == "" {
query.Sorting = "username"
}
excludeAdmin := true excludeAdmin := true
for key := range query.Keywords { for key := range query.Keywords {
str := strings.ToLower(key) str := strings.ToLower(key)

View File

@ -77,7 +77,7 @@ func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListAr
} }
// set query // set query
query, err := a.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return a.SendError(ctx, err) return a.SendError(ctx, err)
} }
@ -292,7 +292,7 @@ func (a *artifactAPI) ListTags(ctx context.Context, params operation.ListTagsPar
return a.SendError(ctx, err) return a.SendError(ctx, err)
} }
// set query // set query
query, err := a.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return a.SendError(ctx, err) return a.SendError(ctx, err)
} }

View File

@ -38,7 +38,7 @@ func (a *auditlogAPI) ListAuditLogs(ctx context.Context, params auditlog.ListAud
if !secCtx.IsAuthenticated() { if !secCtx.IsAuthenticated() {
return a.SendError(ctx, errors.UnauthorizedError(nil).WithMessage(secCtx.GetUsername())) return a.SendError(ctx, errors.UnauthorizedError(nil).WithMessage(secCtx.GetUsername()))
} }
query, err := a.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return a.SendError(ctx, err) return a.SendError(ctx, err)
} }

View File

@ -137,32 +137,26 @@ func (b *BaseAPI) RequireAuthenticated(ctx context.Context) error {
} }
// BuildQuery builds the query model according to the query string // BuildQuery builds the query model according to the query string
func (b *BaseAPI) BuildQuery(ctx context.Context, query *string, pageNumber, pageSize *int64, sorts ...*string) (*q.Query, error) { func (b *BaseAPI) BuildQuery(ctx context.Context, query, sort *string, pageNumber, pageSize *int64) (*q.Query, error) {
var ( var (
qs string qs string
st string
pn int64 pn int64
ps int64 ps int64
) )
if query != nil { if query != nil {
qs = *query qs = *query
} }
if sort != nil {
st = *sort
}
if pageNumber != nil { if pageNumber != nil {
pn = *pageNumber pn = *pageNumber
} }
if pageSize != nil { if pageSize != nil {
ps = *pageSize ps = *pageSize
} }
return q.Build(qs, st, pn, ps)
r, err := q.Build(qs, pn, ps)
if err != nil {
return nil, err
}
if len(sorts) > 0 {
r.Sorting = lib.StringValue(sorts[0])
}
return r, nil
} }
// Links return Links based on the provided pagination information // Links return Links based on the provided pagination information

View File

@ -38,36 +38,44 @@ func (b *baseHandlerTestSuite) SetupSuite() {
} }
func (b *baseHandlerTestSuite) TestBuildQuery() { func (b *baseHandlerTestSuite) TestBuildQuery() {
// nil query string and pagination pointer // nil input
var ( var (
query *string query *string
sort *string
pageNumber *int64 pageNumber *int64
pageSize *int64 pageSize *int64
) )
q, err := b.base.BuildQuery(nil, query, pageNumber, pageSize) q, err := b.base.BuildQuery(nil, query, sort, pageNumber, pageSize)
b.Require().Nil(err) b.Require().Nil(err)
b.Require().NotNil(q) b.Require().NotNil(q)
b.NotNil(q.Keywords) b.NotNil(q.Keywords)
// not nil query string and pagination pointer // not nil input
var ( var (
qs = "q=a=b" qs = "q=a=b"
st = "a,-c"
pn int64 = 1 pn int64 = 1
ps int64 = 10 ps int64 = 10
) )
q, err = b.base.BuildQuery(nil, &qs, &pn, &ps) q, err = b.base.BuildQuery(nil, &qs, &st, &pn, &ps)
b.Require().Nil(err) b.Require().Nil(err)
b.Require().NotNil(q) b.Require().NotNil(q)
b.Equal(int64(1), q.PageNumber) b.Equal(int64(1), q.PageNumber)
b.Equal(int64(10), q.PageSize) b.Equal(int64(10), q.PageSize)
b.NotNil(q.Keywords) b.NotNil(q.Keywords)
b.Require().Len(q.Sorts, 2)
b.Equal("a", q.Sorts[0].Key)
b.False(q.Sorts[0].DESC)
b.Equal("c", q.Sorts[1].Key)
b.True(q.Sorts[1].DESC)
var ( var (
qs1 = "q=a%3Db" qs1 = "q=a%3Db"
st1 = ""
pn1 int64 = 1 pn1 int64 = 1
ps1 int64 = 10 ps1 int64 = 10
) )
q, err = b.base.BuildQuery(nil, &qs1, &pn1, &ps1) q, err = b.base.BuildQuery(nil, &qs1, &st1, &pn1, &ps1)
b.Require().Nil(err) b.Require().Nil(err)
b.Require().NotNil(q) b.Require().NotNil(q)
b.Equal(int64(1), q.PageNumber) b.Equal(int64(1), q.PageNumber)

View File

@ -136,7 +136,7 @@ func (g *gcAPI) GetGCHistory(ctx context.Context, params operation.GetGCHistoryP
if err := g.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceGarbageCollection); err != nil { if err := g.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceGarbageCollection); err != nil {
return g.SendError(ctx, err) return g.SendError(ctx, err)
} }
query, err := g.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := g.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return g.SendError(ctx, err) return g.SendError(ctx, err)
} }

View File

@ -94,7 +94,7 @@ func (ia *immutableAPI) ListImmuRules(ctx context.Context, params operation.List
return ia.SendError(ctx, err) return ia.SendError(ctx, err)
} }
query, err := ia.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := ia.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return ia.SendError(ctx, err) return ia.SendError(ctx, err)
} }

View File

@ -119,7 +119,7 @@ func (api *preheatAPI) ListInstances(ctx context.Context, params operation.ListI
var payload []*models.Instance var payload []*models.Instance
query, err := api.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := api.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return api.SendError(ctx, err) return api.SendError(ctx, err)
} }
@ -320,7 +320,7 @@ func (api *preheatAPI) ListPolicies(ctx context.Context, params operation.ListPo
return api.SendError(ctx, err) return api.SendError(ctx, err)
} }
query, err := api.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := api.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return api.SendError(ctx, err) return api.SendError(ctx, err)
} }
@ -600,7 +600,7 @@ func (api *preheatAPI) ListExecutions(ctx context.Context, params operation.List
return api.SendError(ctx, err) return api.SendError(ctx, err)
} }
query, err := api.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := api.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return api.SendError(ctx, err) return api.SendError(ctx, err)
} }
@ -677,7 +677,7 @@ func (api *preheatAPI) ListTasks(ctx context.Context, params operation.ListTasks
return api.SendError(ctx, err) return api.SendError(ctx, err)
} }
query, err := api.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := api.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return api.SendError(ctx, err) return api.SendError(ctx, err)
} }

View File

@ -260,7 +260,7 @@ func (a *projectAPI) GetLogs(ctx context.Context, params operation.GetLogsParams
if err != nil { if err != nil {
return a.SendError(ctx, err) return a.SendError(ctx, err)
} }
query, err := a.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return a.SendError(ctx, err) return a.SendError(ctx, err)
} }
@ -379,7 +379,6 @@ func (a *projectAPI) HeadProject(ctx context.Context, params operation.HeadProje
func (a *projectAPI) ListProjects(ctx context.Context, params operation.ListProjectsParams) middleware.Responder { func (a *projectAPI) ListProjects(ctx context.Context, params operation.ListProjectsParams) middleware.Responder {
query := q.New(q.KeyWords{}) query := q.New(q.KeyWords{})
query.Sorting = "name"
query.PageNumber = *params.Page query.PageNumber = *params.Page
query.PageSize = *params.PageSize query.PageSize = *params.PageSize
@ -530,7 +529,7 @@ func (a *projectAPI) ListScannerCandidatesOfProject(ctx context.Context, params
return a.SendError(ctx, err) return a.SendError(ctx, err)
} }
query, err := a.BuildQuery(ctx, params.Q, params.Page, params.PageSize, params.Sort) query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return a.SendError(ctx, err) return a.SendError(ctx, err)
} }

View File

@ -57,14 +57,14 @@ func (qa *quotaAPI) ListQuotas(ctx context.Context, params operation.ListQuotasP
return qa.SendError(ctx, err) return qa.SendError(ctx, err)
} }
query := &q.Query{ query, err := qa.BuildQuery(ctx, nil, params.Sort, params.Page, params.PageSize)
Keywords: q.KeyWords{ if err != nil {
"reference": lib.StringValue(params.Reference), return qa.SendError(ctx, err)
"reference_id": lib.StringValue(params.ReferenceID), }
},
PageNumber: *params.Page, query.Keywords = q.KeyWords{
PageSize: *params.PageSize, "reference": lib.StringValue(params.Reference),
Sorting: lib.StringValue(params.Sort), "reference_id": lib.StringValue(params.ReferenceID),
} }
total, err := qa.quotaCtl.Count(ctx, query) total, err := qa.quotaCtl.Count(ctx, query)

View File

@ -99,7 +99,7 @@ func (r *replicationAPI) ListReplicationExecutions(ctx context.Context, params o
if err := r.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceReplication); err != nil { if err := r.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceReplication); err != nil {
return r.SendError(ctx, err) return r.SendError(ctx, err)
} }
query, err := r.BuildQuery(ctx, nil, params.Page, params.PageSize) query, err := r.BuildQuery(ctx, nil, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return r.SendError(ctx, err) return r.SendError(ctx, err)
} }
@ -172,7 +172,7 @@ func (r *replicationAPI) ListReplicationTasks(ctx context.Context, params operat
if err := r.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceReplication); err != nil { if err := r.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceReplication); err != nil {
return r.SendError(ctx, err) return r.SendError(ctx, err)
} }
query, err := r.BuildQuery(ctx, nil, params.Page, params.PageSize) query, err := r.BuildQuery(ctx, nil, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return r.SendError(ctx, err) return r.SendError(ctx, err)
} }

View File

@ -66,7 +66,7 @@ func (r *repositoryAPI) ListRepositories(ctx context.Context, params operation.L
} }
// set query // set query
query, err := r.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := r.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return r.SendError(ctx, err) return r.SendError(ctx, err)
} }

View File

@ -265,7 +265,7 @@ func (r *retentionAPI) OperateRetentionExecution(ctx context.Context, params ope
} }
func (r *retentionAPI) ListRetentionExecutions(ctx context.Context, params operation.ListRetentionExecutionsParams) middleware.Responder { func (r *retentionAPI) ListRetentionExecutions(ctx context.Context, params operation.ListRetentionExecutionsParams) middleware.Responder {
query, err := r.BuildQuery(ctx, nil, params.Page, params.PageSize) query, err := r.BuildQuery(ctx, nil, nil, params.Page, params.PageSize)
if err != nil { if err != nil {
return r.SendError(ctx, err) return r.SendError(ctx, err)
} }
@ -295,7 +295,7 @@ func (r *retentionAPI) ListRetentionExecutions(ctx context.Context, params opera
} }
func (r *retentionAPI) ListRetentionTasks(ctx context.Context, params operation.ListRetentionTasksParams) middleware.Responder { func (r *retentionAPI) ListRetentionTasks(ctx context.Context, params operation.ListRetentionTasksParams) middleware.Responder {
query, err := r.BuildQuery(ctx, nil, params.Page, params.PageSize) query, err := r.BuildQuery(ctx, nil, nil, params.Page, params.PageSize)
if err != nil { if err != nil {
return r.SendError(ctx, err) return r.SendError(ctx, err)
} }

View File

@ -106,7 +106,7 @@ func (rAPI *robotAPI) ListRobot(ctx context.Context, params operation.ListRobotP
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
query, err := rAPI.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := rAPI.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }

View File

@ -142,7 +142,7 @@ func (rAPI *robotV1API) ListRobotV1(ctx context.Context, params operation.ListRo
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
query, err := rAPI.BuildQuery(ctx, params.Q, params.Page, params.PageSize) query, err := rAPI.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }

View File

@ -181,7 +181,7 @@ func (s *scanAllAPI) createOrUpdateScanAllSchedule(ctx context.Context, cronType
func (s *scanAllAPI) getScanAllSchedule(ctx context.Context) (*scheduler.Schedule, error) { func (s *scanAllAPI) getScanAllSchedule(ctx context.Context) (*scheduler.Schedule, error) {
query := q.New(q.KeyWords{"vendor_type": scan.VendorTypeScanAll}) query := q.New(q.KeyWords{"vendor_type": scan.VendorTypeScanAll})
schedules, err := s.scheduler.ListSchedules(ctx, query.First("-creation_time")) schedules, err := s.scheduler.ListSchedules(ctx, query.First(q.NewSort("creation_time", true)))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -240,7 +240,7 @@ func (s *scanAllAPI) getLatestScanAllExecution(ctx context.Context, trigger ...s
query.Keywords["trigger"] = trigger[0] query.Keywords["trigger"] = trigger[0]
} }
executions, err := s.execMgr.List(ctx, query.First("-start_time")) executions, err := s.execMgr.List(ctx, query.First(q.NewSort("start_time", true)))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -125,7 +125,7 @@ func (s *scannerAPI) ListScanners(ctx context.Context, params operation.ListScan
return s.SendError(ctx, err) return s.SendError(ctx, err)
} }
query, err := s.BuildQuery(ctx, params.Q, params.Page, params.PageSize, params.Sort) query, err := s.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil { if err != nil {
return s.SendError(ctx, err) return s.SendError(ctx, err)
} }