// 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" "fmt" "reflect" "strings" "github.com/beego/beego/v2/client/orm" "github.com/goharbor/harbor/src/lib/q" ) // QuerySetter generates the query setter according to the provided model and query. // e.g. // // type Foo struct{ // Field1 string `orm:"-"` // can not filter/sort // Field2 string `orm:"column(customized_field2)"` // support filter by "Field2", "customized_field2" // 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) FilterByField6(ctx context.Context, qs orm.QuerySetter, key string, value interface{}) orm.QuerySetter { // ... // return qs // } // // Defining the method "GetDefaultSorts() []*q.Sort" for the model whose default sorting contains more than one fields // // type Bar struct{ // Field1 string // Field2 string // } // // // Sort by "Field1" desc, "Field2" // // func (b *Bar) GetDefaultSorts() []*q.Sort { // return []*q.Sort{ // { // Key: "Field1", // DESC: true, // }, // { // Key: "Field2", // DESC: false, // }, // } // } func QuerySetter(ctx context.Context, model interface{}, query *q.Query) (orm.QuerySeter, error) { t := reflect.TypeOf(model) if t.Kind() != reflect.Ptr { return nil, fmt.Errorf("<orm.QuerySetter> cannot use non-ptr model struct `%s`", getFullName(t.Elem())) } ormer, err := FromContext(ctx) if err != nil { return nil, err } qs := ormer.QueryTable(model) if query == nil { return qs, nil } metadata := parseModel(model) // set filters qs = setFilters(ctx, qs, query, metadata) // sorting qs = setSorts(qs, query, metadata) // pagination if query.PageSize > 0 { qs = qs.Limit(query.PageSize) if query.PageNumber > 0 { qs = qs.Offset(query.PageSize * (query.PageNumber - 1)) } } return qs, nil } // PaginationOnRawSQL append page information to the raw sql // It should be called after the order by // e.g. // select a, b, c from mytable order by a limit ? offset ? // it appends the " limit ? offset ? " to sql, // and appends the limit value and offset value to the params of this query func PaginationOnRawSQL(query *q.Query, sql string, params []interface{}) (string, []interface{}) { if query != nil && query.PageSize > 0 { sql += ` limit ?` params = append(params, query.PageSize) if query.PageNumber > 0 { sql += ` offset ?` params = append(params, (query.PageNumber-1)*query.PageSize) } } return sql, params } // 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 ol == nil || len(ol.Values) == 0 { qs = qs.Filter(key+"__in", nil) } else { 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 { for _, ds := range meta.DefaultSorts { sorting := ds.Key if ds.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. func getFullName(typ reflect.Type) string { return typ.PkgPath() + "." + typ.Name() }