Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/search-by #331

Merged
merged 7 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gateway/field_presence.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ type presenceInterceptorOptionsDecorator struct {

type presenceInterceptorOption func(*presenceInterceptorOptionsDecorator)

//WithOverrideFieldMask represent an option to override field mask generated by grpc-gateway
// WithOverrideFieldMask represent an option to override field mask generated by grpc-gateway
func WithOverrideFieldMask(d *presenceInterceptorOptionsDecorator) {
d.overrideFieldMask = true
}
Expand Down
8 changes: 4 additions & 4 deletions gateway/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
"github.com/infobloxopen/atlas-app-toolkit/query"
)

//retainFields function extracts the configuration for fields that
//need to be ratained either from gRPC response or from original testRequest
//(in case when gRPC side didn't set any preferences) and retains only
//this fields on outgoing response (dynmap).
// retainFields function extracts the configuration for fields that
// need to be ratained either from gRPC response or from original testRequest
// (in case when gRPC side didn't set any preferences) and retains only
// this fields on outgoing response (dynmap).
func retainFields(ctx context.Context, req *http.Request, dynmap map[string]interface{}) {
fieldsStr := ""
if req != nil {
Expand Down
9 changes: 9 additions & 0 deletions gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ func ClientUnaryInterceptor(parentCtx context.Context, method string, req, reply
}
}

// extracts "_fts" parameters from request
if v := vals.Get(searchQueryKey); v != "" {
s := query.ParseSearching(v)
err = SetCollectionOps(req, s)
if err != nil {
return err
}
}

// extracts "_limit", "_offset", "_page_token" parameters from request
var p *query.Pagination
l := vals.Get(limitQueryKey)
Expand Down
9 changes: 9 additions & 0 deletions gateway/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,12 @@ func GetFieldSelection(req proto.Message) (fieldName string, fs *query.FieldSele
}
return
}

func GetSearching(req proto.Message) (fieldName string, s *query.Searching, err error) {
s = new(query.Searching)
fieldName, err = getAndUnsetOp(req, s, false)
if fieldName == "" {
s = nil
}
return
}
1 change: 1 addition & 0 deletions gateway/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
limitQueryKey = "_limit"
offsetQueryKey = "_offset"
pageTokenQueryKey = "_page_token"
searchQueryKey = "_fts"
pageInfoSizeMetaKey = "status-page-info-size"
pageInfoOffsetMetaKey = "status-page-info-offset"
pageInfoPageTokenMetaKey = "status-page-info-page_token"
Expand Down
28 changes: 28 additions & 0 deletions gorm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ var people []Person
db.Find(&people)
...
```
### Applying query.Searching

```golang
...
db, assoc, err = gorm.ApplySearchingEx(ctx, db, searching, &PersonORM{}, fieldsForFTS, &Person{})
if err != nil {
...
}
db, err = gorm.JoinAssociations(ctx, db, assoc, &PersonORM{})
if err != nil {
...
}
var people []Person
db.Find(&people)
...
```

### Applying everything

Expand All @@ -78,6 +94,18 @@ var people []Person
db.Find(&people)
...
```
### Applying everything with Searching

```golang
...
db, err = gorm.ApplyCollectionOperatorsWithSearchingEx(ctx, db, &PersonORM{}, &Person{}, filtering, sorting, pagination, fields, searching, fieldsForFTS)
if err != nil {
...
}
var people []Person
db.Find(&people)
...
```


## Transaction Management
Expand Down
45 changes: 45 additions & 0 deletions gorm/collection_operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ type PaginationConverter interface {
PaginationToGorm(ctx context.Context, p *query.Pagination) (offset, limit int32)
}

type SearchingConverter interface {
SearchingToGorm(ctx context.Context, s *query.Searching, fieldsForFTS []string, obj interface{}) (string, error)
}

type CollectionOperatorsConverter interface {
FilteringConditionConverter
SortingCriteriaConverter
FieldSelectionConverter
PaginationConverter
SearchingConverter
}

func ApplyCollectionOperatorsEx(ctx context.Context, db *gorm.DB, obj interface{}, c CollectionOperatorsConverter, f *query.Filtering, s *query.Sorting, p *query.Pagination, fs *query.FieldSelection) (*gorm.DB, error) {
Expand Down Expand Up @@ -62,6 +67,46 @@ func ApplyCollectionOperatorsEx(ctx context.Context, db *gorm.DB, obj interface{
return db, nil
}

func ApplyCollectionOperatorsWithSearchingEx(ctx context.Context, db *gorm.DB, obj interface{}, c CollectionOperatorsConverter, f *query.Filtering, s *query.Sorting, p *query.Pagination, fs *query.FieldSelection, sc *query.Searching, fieldsForFTS []string) (*gorm.DB, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is a new operator with a separate function, it won't be handled in the normal protoc-gen-gorm generated handlers without changes there.

db, err := ApplyCollectionOperatorsEx(ctx, db, obj, c, f, s, p, fs)
if err != nil {
return nil, err
}

db, err = ApplySearchingEx(ctx, db, sc, obj, fieldsForFTS, c)
if err != nil {
return nil, err
}

return db, nil
}

// ApplySearchingEx applies searching operator s to gorm instance db.
func ApplySearchingEx(ctx context.Context, db *gorm.DB, s *query.Searching, obj interface{}, fieldsForFTS []string, c SearchingConverter) (*gorm.DB, error) {
str, err := c.SearchingToGorm(ctx, s, fieldsForFTS, obj)
if err != nil {
return nil, err
}
if s != nil && s.Query != "" {
s.Query = strings.TrimSpace(s.Query)
s.Query = strings.ReplaceAll(s.Query, ":", " ")
splChar := []string{"(", ")", "|", "+", "<", "'", "&", "!", "%", ";"}
for _, spl := range splChar {
if strings.Contains(s.Query, spl) {
s.Query = ""
return db.Where(str, s.Query), nil
}
}
s.Query = strings.Join(strings.Fields(s.Query), " ")
if s.Query != "" {
s.Query = strings.ReplaceAll(s.Query, " ", " & ")
s.Query = s.Query + ":*"
}
return db.Where(str, s.Query), nil
}
return db, nil
}

// ApplyFiltering applies filtering operator f to gorm instance db.
func ApplyFilteringEx(ctx context.Context, db *gorm.DB, f *query.Filtering, obj interface{}, c FilteringConditionConverter) (*gorm.DB, map[string]struct{}, error) {
str, args, assocToJoin, err := FilteringToGormEx(ctx, f, obj, c)
Expand Down
11 changes: 11 additions & 0 deletions gorm/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,16 @@ type DefaultSortingCriteriaConverter struct{}
// DefaultPaginationConverter performs default convertion for Paging collection operator
type DefaultPaginationConverter struct{}

// DefaultSearchingConverter performs default convertion for Searching operator
type DefaultSearchingConverter struct{}

// DefaultPbToOrmConverter performs default convertion for all collection operators
type DefaultPbToOrmConverter struct {
DefaultFilteringConditionConverter
DefaultSortingCriteriaConverter
DefaultFieldSelectionConverter
DefaultPaginationConverter
DefaultSearchingConverter
}

// NewDefaultPbToOrmConverter creates default converter for all collection operators
Expand All @@ -45,6 +49,7 @@ func NewDefaultPbToOrmConverter(pb proto.Message) CollectionOperatorsConverter {
DefaultSortingCriteriaConverter{},
DefaultFieldSelectionConverter{},
DefaultPaginationConverter{},
DefaultSearchingConverter{},
}
}

Expand Down Expand Up @@ -360,3 +365,9 @@ func (converter *DefaultPaginationConverter) PaginationToGorm(ctx context.Contex
}
return 0, 0
}

func (converter *DefaultSearchingConverter) SearchingToGorm(ctx context.Context, s *query.Searching, fieldsForFTS []string, obj interface{}) (string, error) {
mask := GetFullTextSearchDBMask(obj, fieldsForFTS, " ")
fullTextSearchQuery := FormFullTextSearchQuery(mask)
return fullTextSearchQuery, nil
}
2 changes: 1 addition & 1 deletion gorm/filtering.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func FilterStringToGorm(ctx context.Context, filter string, obj interface{}, pb
return FilteringToGormEx(ctx, f, obj, c)
}

//Deprecated: Use FilteringToGormEx instead
// Deprecated: Use FilteringToGormEx instead
// FilteringToGorm returns GORM Plain SQL representation of the filtering expression.
func FilteringToGorm(ctx context.Context, m *query.Filtering, obj interface{}, pb proto.Message) (string, []interface{}, map[string]struct{}, error) {
c := &DefaultFilteringConditionConverter{&DefaultFilteringConditionProcessor{pb}}
Expand Down
58 changes: 58 additions & 0 deletions gorm/searching.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package gorm

import (
"reflect"
"time"
)

// GetFullTextSearchDBMask ...
func GetFullTextSearchDBMask(object interface{}, fields []string, separator string) string {
mask := ""
objectVal := indirectValue(reflect.ValueOf(object))
if objectVal.Kind() != reflect.Struct {
return mask
}
fieldsSize := len(fields)
for i, fieldName := range fields {
fieldVal := objectVal.FieldByName(camelCase(fieldName))
if !fieldVal.IsValid() {
continue
}
underlyingVal := indirectValue(fieldVal)
if !underlyingVal.IsValid() {
switch fieldVal.Interface().(type) {
case *time.Time:
underlyingVal = fieldVal
default:
continue
}
}
switch underlyingVal.Interface().(type) {
case int32:
mask += fieldName
case string:
mask += fieldName
mask += " || '" + separator + "' || "
mask += "replace(" + fieldName + ", '@', ' ')"
mask += " || '" + separator + "' || "
mask += "replace(" + fieldName + ", '.', ' ')"
case *time.Time:
mask += "coalesce(to_char(" + fieldName + ", 'MM/DD/YY HH:MI pm'), '')"
case bool:
mask += fieldName
default:
continue
}
if i != fieldsSize-1 {
mask += " || '" + separator + "' || "
}
}

return mask
}

// FormFullTextSearchQuery ...
func FormFullTextSearchQuery(mask string) string {
fullTextSearchQuery := "to_tsvector('simple', " + mask + ") @@ to_tsquery('simple', ?)"
return fullTextSearchQuery
}
25 changes: 23 additions & 2 deletions gorm/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func HandleFieldPath(ctx context.Context, fieldPath []string, obj interface{}) (
return dbPath, "", nil
}

//HandleJSONFiledPath translate field path to JSONB path for postgres jsonb
// HandleJSONFiledPath translate field path to JSONB path for postgres jsonb
func HandleJSONFieldPath(ctx context.Context, fieldPath []string, obj interface{}, values ...string) (string, string, error) {
operator := "#>>"
if isRawJSON(values...) {
Expand Down Expand Up @@ -82,7 +82,7 @@ func isRawJSON(values ...string) bool {
return true
}

//TODO: add supprt for embeded objects
// TODO: add supprt for embeded objects
func IsJSONCondition(ctx context.Context, fieldPath []string, obj interface{}) bool {
fieldName := util.Camel(fieldPath[0])
objType := indirectType(reflect.TypeOf(obj))
Expand Down Expand Up @@ -189,6 +189,17 @@ func indirectType(t reflect.Type) reflect.Type {
}
}

func indirectValue(val reflect.Value) reflect.Value {
for {
switch val.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Array:
val = val.Elem()
default:
return val
}
}
}

func isModel(t reflect.Type) bool {
kind := t.Kind()
_, isValuer := reflect.Zero(t).Interface().(driver.Valuer)
Expand All @@ -214,3 +225,13 @@ type EmptyFieldPathError struct {
func (e *EmptyFieldPathError) Error() string {
return fmt.Sprintf("Empty field path is not allowed")
}

func camelCase(v string) string {
sp := strings.Split(v, "_")
r := make([]string, len(sp))
for i, v := range sp {
r[i] = strings.ToUpper(v[:1]) + v[1:]
}

return strings.Join(r, "")
}
9 changes: 9 additions & 0 deletions query/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ These types are:
- `infoblox.api.Pagination`
- `infoblox.api.PageInfo`(used in response)
- `infoblox.api.FieldSelection`
- `infoblox.api.Searching`

## Enabling *collection operators* in your application

Expand All @@ -22,6 +23,7 @@ message MyListRequest {
infoblox.api.Sorting sorting = 2;
infoblox.api.Pagination pagination = 3;
infoblox.api.FieldSelection fields = 4;
infoblox.api.Searching searching = 5;
}
```

Expand Down Expand Up @@ -162,3 +164,10 @@ server.WithGateway(
)
)
```
## Searching

The syntax of REST representation of `infoblox.api.Searching` is the following.

| Request Parameter | Description |
| ----------------- |------------------------------------------|
| _fts | A string expression which performs a full-text-search on the DB |
abalaven marked this conversation as resolved.
Show resolved Hide resolved
Loading