From f7cf63c2bfcab4cb7be83233471075ef4b601b94 Mon Sep 17 00:00:00 2001 From: Eden Kirin Date: Thu, 27 Jun 2024 13:49:28 +0200 Subject: [PATCH 1/3] Change separator --- app/main.go | 42 ++++++++++++++----- app/repository/method_list_test.go | 8 ++-- app/repository/smartfilter/filterfield.go | 1 + app/repository/smartfilter/smartfilter.go | 2 +- .../smartfilter/smartfilter_test.go | 24 +++++------ 5 files changed, 49 insertions(+), 28 deletions(-) diff --git a/app/main.go b/app/main.go index 15d2f69..02d38c3 100644 --- a/app/main.go +++ b/app/main.go @@ -20,19 +20,20 @@ var ( ) type CertFilter struct { - Alive *bool `filterfield:"field=alive,operator=EQ"` - SerialNumber *string `filterfield:"field=serial_number,operator=NE"` - SerialNumberContains *string `filterfield:"field=serial_number,operator=LIKE"` - IssuerContains *string `filterfield:"field=issuer,operator=ILIKE"` - Id *uuid.UUID `filterfield:"field=id,operator=EQ"` - Ids *[]uuid.UUID `filterfield:"field=id,operator=IN"` - IdsNot *[]string `filterfield:"field=id,operator=NOT_IN"` - CreatedAt_Lt *time.Time `filterfield:"field=created_at,operator=LT"` - Timestamps *[]time.Time `filterfield:"field=created_at,operator=IN"` + Alive *bool `filterfield:"field=alive;operator=EQ"` + SerialNumber *string `filterfield:"field=serial_number;operator=NE"` + SerialNumberContains *string `filterfield:"field=serial_number;operator=LIKE"` + IssuerContains *string `filterfield:"field=issuer;operator=ILIKE"` + Id *uuid.UUID `filterfield:"field=id;operator=EQ"` + Ids *[]uuid.UUID `filterfield:"field=id;operator=IN"` + IdsNot *[]string `filterfield:"field=id;operator=NOT_IN"` + CreatedAt_Lt *time.Time `filterfield:"field=created_at;operator=LT"` + Timestamps *[]time.Time `filterfield:"field=created_at;operator=IN"` + // CompanyIsActive *bool `filterfield:"joins=companies;field=is_active;operator=EQ"` } type CompanyFilter struct { - IsActive *bool `filterfield:"field=is_active,operator=EQ"` + IsActive *bool `filterfield:"field=is_active;operator=EQ"` } func doMagic(db *gorm.DB) { @@ -93,6 +94,24 @@ func doList(db *gorm.DB) { } } +func doListWithJoins(db *gorm.DB) { + repo := repository.RepoBase[models.Cert]{} + repo.Init(db, nil) + + filter := CertFilter{ + Alive: &TRUE, + } + + certs, err := repo.List(filter, nil) + if err != nil { + panic(err) + } + + for n, cert := range *certs { + fmt.Printf(">> [%d] %+v %s (alive %t)\n", n, cert.Id, cert.CreatedAt, cert.Alive) + } +} + func doCount(db *gorm.DB) { repo := repository.RepoBase[models.Cert]{} repo.Init(db, nil) @@ -184,7 +203,8 @@ func main() { db := db.InitDB() // doMagic(db) - // doList(db) + doList(db) + // doListWithJoins(db) // doCount(db) // doSave(db) doDelete(db) diff --git a/app/repository/method_list_test.go b/app/repository/method_list_test.go index 16b1195..cd2341a 100644 --- a/app/repository/method_list_test.go +++ b/app/repository/method_list_test.go @@ -43,10 +43,10 @@ func (m MyModel) TableName() string { } type MyModelFilter struct { - Id *uuid.UUID `filterfield:"field=id,operator=EQ"` - Ids *[]uuid.UUID `filterfield:"field=id,operator=IN"` - Value *string `filterfield:"field=value,operator=EQ"` - CntGT *int `filterfield:"field=cnt,operator=GT"` + Id *uuid.UUID `filterfield:"field=id;operator=EQ"` + Ids *[]uuid.UUID `filterfield:"field=id;operator=IN"` + Value *string `filterfield:"field=value;operator=EQ"` + CntGT *int `filterfield:"field=cnt;operator=GT"` } func TestListMethod(t *testing.T) { diff --git a/app/repository/smartfilter/filterfield.go b/app/repository/smartfilter/filterfield.go index a4cf4b8..8cc2c06 100644 --- a/app/repository/smartfilter/filterfield.go +++ b/app/repository/smartfilter/filterfield.go @@ -11,6 +11,7 @@ import ( type FilterField struct { Name string Operator Operator + Joins []string valueKind reflect.Kind boolValue *bool diff --git a/app/repository/smartfilter/smartfilter.go b/app/repository/smartfilter/smartfilter.go index b77ba4d..84bd8d9 100644 --- a/app/repository/smartfilter/smartfilter.go +++ b/app/repository/smartfilter/smartfilter.go @@ -11,7 +11,7 @@ import ( ) const TAG_NAME = "filterfield" -const TAG_PAIRS_SEPARATOR = "," +const TAG_PAIRS_SEPARATOR = ";" const TAG_KEYVALUE_SEPARATOR = "=" type handlerFunc func(query *gorm.DB, tableName string, filterField *FilterField) *gorm.DB diff --git a/app/repository/smartfilter/smartfilter_test.go b/app/repository/smartfilter/smartfilter_test.go index 13ab166..c9834d6 100644 --- a/app/repository/smartfilter/smartfilter_test.go +++ b/app/repository/smartfilter/smartfilter_test.go @@ -101,11 +101,11 @@ func TestGetFilterFields(t *testing.T) { t.Run("Skip nil fields", func(t *testing.T) { type TestFilter struct { - Alive *bool `filterfield:"alive,EQ"` - Id *int64 `filterfield:"id,EQ"` - Ids *[]uint `filterfield:"id,IN"` - IdsNot *[]uint `filterfield:"id,NOT_IN"` - FirstName *string `filterfield:"first_name,EQ"` + Alive *bool `filterfield:"alive;EQ"` + Id *int64 `filterfield:"id;EQ"` + Ids *[]uint `filterfield:"id;IN"` + IdsNot *[]uint `filterfield:"id;NOT_IN"` + FirstName *string `filterfield:"first_name;EQ"` } filter := TestFilter{} result := getFilterFields(filter) @@ -119,7 +119,7 @@ func TestGetFilterFields(t *testing.T) { ) type TestFilter struct { Alive *bool - Id *int64 `funnytag:"created_at,LT"` + Id *int64 `funnytag:"created_at;LT"` } filter := TestFilter{ Alive: &alive, @@ -140,7 +140,7 @@ func TestFilterField(t *testing.T) { testCases := []TagParseTestCase{ { name: "Parse without spaces", - tagValue: "field=field_1,operator=EQ", + tagValue: "field=field_1;operator=EQ", expected: FilterField{ Name: "field_1", Operator: OperatorEQ, @@ -148,7 +148,7 @@ func TestFilterField(t *testing.T) { }, { name: "Parse spaces between pairs", - tagValue: " field=field_2 , operator=LT ", + tagValue: " field=field_2 ; operator=LT ", expected: FilterField{ Name: "field_2", Operator: OperatorLT, @@ -156,7 +156,7 @@ func TestFilterField(t *testing.T) { }, { name: "Parse spaces between around keys and values", - tagValue: "operator = LIKE , field = field_3", + tagValue: "operator = LIKE ; field = field_3", expected: FilterField{ Name: "field_3", Operator: OperatorLIKE, @@ -174,19 +174,19 @@ func TestFilterField(t *testing.T) { } t.Run("Fail on invalid tag value", func(t *testing.T) { - filterField, err := newFilterField("field=field_1=fail, operator=EQ") + filterField, err := newFilterField("field=field_1=fail; operator=EQ") assert.Nil(t, filterField) assert.EqualError(t, err, "invalid tag value: field=field_1=fail") }) t.Run("Fail on invalid operator", func(t *testing.T) { - filterField, err := newFilterField("field=field_1, operator=FAIL") + filterField, err := newFilterField("field=field_1; operator=FAIL") assert.Nil(t, filterField) assert.EqualError(t, err, "unknown operator: FAIL") }) t.Run("Fail on invalid value key", func(t *testing.T) { - filterField, err := newFilterField("failkey=field_1, operator=FAIL") + filterField, err := newFilterField("failkey=field_1; operator=FAIL") assert.Nil(t, filterField) assert.EqualError(t, err, "invalid value key: failkey") }) From f50aaca44d059c9a6276a8195ad785c760bb1747 Mon Sep 17 00:00:00 2001 From: Eden Kirin Date: Sat, 29 Jun 2024 15:37:59 +0200 Subject: [PATCH 2/3] ApplyQuery interface --- app/main.go | 21 ++++++--- app/repository/smartfilter/filterfield.go | 1 - app/repository/smartfilter/smartfilter.go | 44 ++++++++++++++----- .../smartfilter/smartfilter_test.go | 30 +++++++++++++ 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/app/main.go b/app/main.go index 02d38c3..7df139f 100644 --- a/app/main.go +++ b/app/main.go @@ -29,7 +29,18 @@ type CertFilter struct { IdsNot *[]string `filterfield:"field=id;operator=NOT_IN"` CreatedAt_Lt *time.Time `filterfield:"field=created_at;operator=LT"` Timestamps *[]time.Time `filterfield:"field=created_at;operator=IN"` - // CompanyIsActive *bool `filterfield:"joins=companies;field=is_active;operator=EQ"` + CompanyIsActive *bool +} + +func (f CertFilter) ApplyQuery(query *gorm.DB) *gorm.DB { + if f.CompanyIsActive != nil { + query = query.Joins( + fmt.Sprintf( + "JOIN companies ON certificates.company_id = companies.id WHERE companies.is_active = %t", + *f.CompanyIsActive, + )) + } + return query } type CompanyFilter struct { @@ -99,7 +110,7 @@ func doListWithJoins(db *gorm.DB) { repo.Init(db, nil) filter := CertFilter{ - Alive: &TRUE, + CompanyIsActive: &FALSE, } certs, err := repo.List(filter, nil) @@ -203,11 +214,11 @@ func main() { db := db.InitDB() // doMagic(db) - doList(db) - // doListWithJoins(db) + // doList(db) + doListWithJoins(db) // doCount(db) // doSave(db) - doDelete(db) + // doDelete(db) // doGet(db) // doExists(db) // inheritance.DoInheritanceTest() diff --git a/app/repository/smartfilter/filterfield.go b/app/repository/smartfilter/filterfield.go index 8cc2c06..a4cf4b8 100644 --- a/app/repository/smartfilter/filterfield.go +++ b/app/repository/smartfilter/filterfield.go @@ -11,7 +11,6 @@ import ( type FilterField struct { Name string Operator Operator - Joins []string valueKind reflect.Kind boolValue *bool diff --git a/app/repository/smartfilter/smartfilter.go b/app/repository/smartfilter/smartfilter.go index 84bd8d9..606e821 100644 --- a/app/repository/smartfilter/smartfilter.go +++ b/app/repository/smartfilter/smartfilter.go @@ -12,10 +12,15 @@ import ( const TAG_NAME = "filterfield" const TAG_PAIRS_SEPARATOR = ";" +const TAG_LIST_SEPARATOR = "," const TAG_KEYVALUE_SEPARATOR = "=" type handlerFunc func(query *gorm.DB, tableName string, filterField *FilterField) *gorm.DB +type QueryApplier interface { + ApplyQuery(query *gorm.DB) *gorm.DB +} + var operatorHandlers = map[Operator]handlerFunc{ OperatorEQ: handleOperatorEQ, OperatorNE: handleOperatorNE, @@ -67,15 +72,20 @@ func getFilterFields(filter interface{}) []ReflectedStructField { return res } +func getQueryApplierInterface(filter interface{}) QueryApplier { + queryApplier, ok := filter.(QueryApplier) + if ok { + return queryApplier + } + return nil +} + func ToQuery(model schema.Tabler, filter interface{}, query *gorm.DB) (*gorm.DB, error) { st := reflect.TypeOf(filter) tableName := model.TableName() modelName := st.Name() - fmt.Printf("Table name: %s\n", tableName) - fmt.Printf("Model name: %s\n", modelName) - fields := getFilterFields(filter) for _, field := range fields { filterField, err := newFilterField(field.tagValue) @@ -97,22 +107,36 @@ func ToQuery(model schema.Tabler, filter interface{}, query *gorm.DB) (*gorm.DB, } } + // apply custom filters, if interface exists + queryApplier := getQueryApplierInterface(filter) + if queryApplier != nil { + query = queryApplier.ApplyQuery(query) + } + return query, nil } +func splitTrim(value string, separator string) []string { + var out []string = []string{} + for _, s := range strings.Split(value, separator) { + if len(s) == 0 { + continue + } + out = append(out, strings.TrimSpace(s)) + } + return out +} + func newFilterField(tagValue string) (*FilterField, error) { filterField := FilterField{} - tagValue = strings.TrimSpace(tagValue) - pairs := strings.Split(tagValue, TAG_PAIRS_SEPARATOR) - - for _, pair := range pairs { - kvs := strings.Split(pair, TAG_KEYVALUE_SEPARATOR) + for _, pair := range splitTrim(tagValue, TAG_PAIRS_SEPARATOR) { + kvs := splitTrim(pair, TAG_KEYVALUE_SEPARATOR) if len(kvs) != 2 { return nil, fmt.Errorf("invalid tag value: %s", strings.TrimSpace(pair)) } - key := strings.TrimSpace(kvs[0]) - value := strings.TrimSpace(kvs[1]) + key := kvs[0] + value := kvs[1] switch key { case "field": diff --git a/app/repository/smartfilter/smartfilter_test.go b/app/repository/smartfilter/smartfilter_test.go index c9834d6..554e4a2 100644 --- a/app/repository/smartfilter/smartfilter_test.go +++ b/app/repository/smartfilter/smartfilter_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "gorm.io/gorm" ) func TestGetFilterFields(t *testing.T) { @@ -203,3 +204,32 @@ func TestFilterField(t *testing.T) { assert.EqualError(t, err, "missing operator in tag: field=field_1") }) } + +type filterWithoutQueryApplier struct{} + +type filterWithQueryApplier struct{} + +func (f filterWithQueryApplier) ApplyQuery(query *gorm.DB) *gorm.DB { + return query +} + +func TestSmartfilterApplyQuery(t *testing.T) { + + t.Run("Get query applier interface - without interface", func(t *testing.T) { + f := filterWithoutQueryApplier{} + queryApplier := getQueryApplierInterface(f) + assert.Nil(t, queryApplier) + }) + + t.Run("Get query applier interface - with interface", func(t *testing.T) { + f := filterWithQueryApplier{} + queryApplier := getQueryApplierInterface(f) + assert.NotNil(t, queryApplier) + }) + + // t.Run("Get query applier interface - call interface function", func(t *testing.T) { + // f := filterWithQueryApplier{} + // queryApplier := getQueryApplierInterface(f) + // assert.NotNil(t, queryApplier) + // }) +} From 86f4550b0845417e984b62c27ec7a1280d4f3d3e Mon Sep 17 00:00:00 2001 From: Eden Kirin Date: Sun, 14 Jul 2024 13:17:04 +0200 Subject: [PATCH 3/3] Finish --- app/repository/smartfilter/smartfilter_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/repository/smartfilter/smartfilter_test.go b/app/repository/smartfilter/smartfilter_test.go index 554e4a2..0a54e7d 100644 --- a/app/repository/smartfilter/smartfilter_test.go +++ b/app/repository/smartfilter/smartfilter_test.go @@ -226,10 +226,4 @@ func TestSmartfilterApplyQuery(t *testing.T) { queryApplier := getQueryApplierInterface(f) assert.NotNil(t, queryApplier) }) - - // t.Run("Get query applier interface - call interface function", func(t *testing.T) { - // f := filterWithQueryApplier{} - // queryApplier := getQueryApplierInterface(f) - // assert.NotNil(t, queryApplier) - // }) }