Skip to content

Commit

Permalink
bugfix db update partial option
Browse files Browse the repository at this point in the history
  • Loading branch information
agungdwiprasetyo committed Apr 30, 2024
1 parent 4cfcdf7 commit 82109bb
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 49 deletions.
16 changes: 16 additions & 0 deletions candihelper/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,3 +576,19 @@ func ElementInSlice[T comparable](el T, list []T) bool {
}
return false
}

func ReflectTypeUnwrapPtr(f reflect.Type) reflect.Type {
if f.Kind() == reflect.Ptr {
return ReflectTypeUnwrapPtr(f.Elem())
}
return f
}
func ReflectValueUnwrapPtr(f reflect.Value) reflect.Value {
if f.Kind() == reflect.Ptr {
if f.IsNil() {
return f
}
return ReflectValueUnwrapPtr(f.Elem())
}
return f
}
10 changes: 3 additions & 7 deletions candihelper/query_param.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,12 @@ func ParseFromQueryParam(query URLQueryGetter, target interface{}) (err error) {
func ParseToQueryParam(source interface{}) (s string) {
defer func() { recover() }()

pValue := reflect.ValueOf(source)
pType := reflect.TypeOf(source)
if pValue.Kind() == reflect.Ptr {
pValue = pValue.Elem()
pType = pType.Elem()
}
pValue := ReflectValueUnwrapPtr(reflect.ValueOf(source))
pType := ReflectTypeUnwrapPtr(reflect.TypeOf(source))

var uri []string
for i := 0; i < pValue.NumField(); i++ {
field := pValue.Field(i)
field := ReflectValueUnwrapPtr(pValue.Field(i))
typ := pType.Field(i)

if typ.PkgPath != "" && !typ.Anonymous { // unexported
Expand Down
16 changes: 8 additions & 8 deletions candihelper/query_param_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,21 @@ func TestParseFromQueryParam(t *testing.T) {

func TestParseToQueryParam(t *testing.T) {
type VariantRequestParams struct {
Filter string `json:"filter,omitempty"`
FilterQuery string `json:"filter[query],omitempty"`
FilterSkuNo string `json:"filter[skuNo],omitempty"`
Page int `json:"page"`
Limit int `json:"limit"`
Ignore string `json:"-"`
Filter **string `json:"filter,omitempty"`
FilterQuery string `json:"filter[query],omitempty"`
FilterSkuNo string `json:"filter[skuNo],omitempty"`
Page int `json:"page"`
Limit int `json:"limit"`
Ignore string `json:"-"`
}

var param VariantRequestParams
param.Filter = "product"
param.Filter = WrapPtr(WrapPtr("product"))
param.FilterQuery = "kulkas"
param.FilterSkuNo = ""
param.Page = 1
param.Limit = 10

want := "filter=product&filter[query]=kulkas&page=1&limit=10"
assert.Equal(t, want, ParseToQueryParam(&param))
assert.Equal(t, want, ParseToQueryParam(WrapPtr(WrapPtr(WrapPtr(param)))))
}
51 changes: 34 additions & 17 deletions candishared/database_update_tools.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package candishared

import (
"database/sql/driver"
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"time"

"github.com/golangid/candi/candihelper"
)
Expand Down Expand Up @@ -37,25 +41,25 @@ func DBUpdateSetIgnoredFields(fields ...string) DBUpdateOptionFunc {
}

// DBUpdateGORMExtractorKey struct field key extractor for gorm model
func DBUpdateGORMExtractorKey(structField reflect.StructField) string {
func DBUpdateGORMExtractorKey(structField reflect.StructField) (string, bool) {
gormTag := structField.Tag.Get("gorm")
if strings.HasPrefix(gormTag, "column:") {
return strings.Split(strings.TrimPrefix(gormTag, "column:"), ";")[0]
return strings.Split(strings.TrimPrefix(gormTag, "column:"), ";")[0], false
}
return candihelper.ToDelimited(structField.Name, '_')
return candihelper.ToDelimited(structField.Name, '_'), false
}

// DBUpdateMongoExtractorKey struct field key extractor for mongo model
func DBUpdateMongoExtractorKey(structField reflect.StructField) string {
func DBUpdateMongoExtractorKey(structField reflect.StructField) (string, bool) {
if bsonTag := strings.TrimSuffix(structField.Tag.Get("bson"), ",omitempty"); bsonTag != "" {
return bsonTag
return bsonTag, true
}
return candihelper.ToDelimited(structField.Name, '_')
return candihelper.ToDelimited(structField.Name, '_'), false
}

// DBUpdateTools for construct selected field to update
type DBUpdateTools struct {
KeyExtractorFunc func(structTag reflect.StructField) string
KeyExtractorFunc func(structTag reflect.StructField) (key string, mustSet bool)
IgnoredFields []string
}

Expand All @@ -70,17 +74,13 @@ func (d *DBUpdateTools) parseOption(opts ...DBUpdateOptionFunc) (o partialUpdate
func (d DBUpdateTools) ToMap(data interface{}, opts ...DBUpdateOptionFunc) map[string]interface{} {
opt := d.parseOption(opts...)

dataValue := reflect.ValueOf(data)
dataType := reflect.TypeOf(data)
if dataValue.Kind() == reflect.Ptr {
dataValue = dataValue.Elem()
dataType = dataType.Elem()
}
dataValue := candihelper.ReflectValueUnwrapPtr(reflect.ValueOf(data))
dataType := candihelper.ReflectTypeUnwrapPtr(reflect.TypeOf(data))
isPartial := len(opt.updateFields) > 0 || len(opt.ignoreFields) > 0

updateFields := make(map[string]interface{}, 0)
for i := 0; i < dataValue.NumField(); i++ {
fieldValue := dataValue.Field(i)
fieldValue := candihelper.ReflectValueUnwrapPtr(dataValue.Field(i))
fieldType := dataType.Field(i)

if fieldType.Anonymous {
Expand All @@ -90,18 +90,35 @@ func (d DBUpdateTools) ToMap(data interface{}, opts ...DBUpdateOptionFunc) map[s
continue
}

var mustSet bool
key := strings.TrimSuffix(fieldType.Tag.Get("json"), ",omitempty")
if d.KeyExtractorFunc != nil {
key = d.KeyExtractorFunc(fieldType)
key, mustSet = d.KeyExtractorFunc(fieldType)
}
isIgnore, _ := strconv.ParseBool(fieldType.Tag.Get("ignoreUpdate"))
if key == "" || key == "-" || isIgnore {
continue
}

isSet := true
val := fieldValue.Interface()
if fieldValue.Kind() == reflect.Pointer && !fieldValue.IsNil() {
val = fieldValue.Elem().Interface()
if candihelper.ReflectTypeUnwrapPtr(fieldType.Type).Kind() == reflect.Struct {
switch t := val.(type) {
case driver.Valuer:
val, _ = t.Value()
case time.Time:
case fmt.Stringer:
val = t.String()
case json.Marshaler:
jsVal, _ := t.MarshalJSON()
val = string(jsVal)
default:
isSet = false
}
}

if !isSet && !mustSet {
continue
}

if !isPartial {
Expand Down
75 changes: 58 additions & 17 deletions candishared/database_update_tools_test.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
package candishared

import (
"database/sql"
"testing"
"time"

"github.com/golangid/candi/candihelper"
"github.com/stretchr/testify/assert"
)

func TestDBUpdateTools(t *testing.T) {
func TestDBUpdateToolsSQL(t *testing.T) {
type SubModel struct {
Title string `gorm:"column:title" json:"title"`
Profile string `gorm:"column:profile" json:"profile"`
CityAddress string `gorm:"type:text"`
Title string `gorm:"column:title" json:"title"`
Profile string `gorm:"column:profile" json:"profile"`
ActivatedAt sql.NullTime `gorm:"column:activated_at" json:"activatedAt"`
CityAddress string `gorm:"type:text"`
}

type Model struct {
ID int `gorm:"column:db_id;" json:"id"`
Name *string `gorm:"column:db_name;" json:"name"`
Address string `gorm:"column:db_address" json:"address"`
ID int `gorm:"column:db_id;" json:"id"`
Name *string `gorm:"column:db_name;" json:"name"`
Address string `gorm:"column:db_address" json:"address"`
No int
IgnoreMe SubModel `gorm:"column:test" json:"ignoreMe" ignoreUpdate:"true"`
Rel SubModel `gorm:"foreignKey:ID" json:"rel"`
SubModel
}
var updated map[string]any

updated := DBUpdateTools{KeyExtractorFunc: DBUpdateGORMExtractorKey}.ToMap(
updated = DBUpdateTools{KeyExtractorFunc: DBUpdateGORMExtractorKey}.ToMap(
&Model{ID: 1, Name: candihelper.ToStringPtr("01"), Address: "street", SubModel: SubModel{Title: "test", CityAddress: "Jakarta"}, Rel: SubModel{Title: "rel sub"}},
DBUpdateSetUpdatedFields("ID", "Name", "Title", "CityAddress"),
)
Expand All @@ -33,16 +37,53 @@ func TestDBUpdateTools(t *testing.T) {
assert.Equal(t, "test", updated["title"])
assert.Equal(t, "Jakarta", updated["city_address"])

updated = DBUpdateTools{}.ToMap(
Model{ID: 1, Name: candihelper.ToStringPtr("01"), Address: "street", SubModel: SubModel{Title: "test"}},
updated = DBUpdateTools{KeyExtractorFunc: DBUpdateGORMExtractorKey}.ToMap(
Model{ID: 1, Name: candihelper.ToStringPtr("01"), Address: "street", SubModel: SubModel{Title: "test", ActivatedAt: sql.NullTime{Valid: true}}},
DBUpdateSetIgnoredFields("ID", "Name", "Title"),
)
assert.Equal(t, 3, len(updated))
assert.Equal(t, "street", updated["address"])
assert.Equal(t, 5, len(updated))
assert.Equal(t, "street", updated["db_address"])

updated = DBUpdateTools{KeyExtractorFunc: DBUpdateGORMExtractorKey}.ToMap(
Model{
No: 10, Rel: SubModel{Title: "001"},
SubModel: SubModel{ActivatedAt: sql.NullTime{Valid: true, Time: time.Now()}, CityAddress: "Jakarta"},
},
)
assert.Equal(t, 8, len(updated))
assert.Equal(t, "Jakarta", updated["city_address"])
assert.Equal(t, 10, updated["no"])
}

func TestDBUpdateToolsMongo(t *testing.T) {
type SubModel struct {
Title string `bson:"title" json:"title"`
Profile string `bson:"profile" json:"profile"`
CityAddress string `bson:"city_address"`
}
type Model struct {
ID int `bson:"db_id" json:"id"`
Name *string `bson:"db_name" json:"name"`
Address string `bson:"db_address" json:"address"`
IgnoreMe SubModel `bson:"-"`
Rel SubModel `bson:"rel" json:"rel"`
SubModel
}

var updated map[string]any

updated = DBUpdateTools{KeyExtractorFunc: DBUpdateMongoExtractorKey}.ToMap(
&Model{ID: 1, Name: candihelper.ToStringPtr("01"), Address: "street", SubModel: SubModel{Title: "test", CityAddress: "Jakarta"}, Rel: SubModel{Title: "rel sub"}},
DBUpdateSetUpdatedFields("ID", "Name", "Title", "CityAddress"),
)
assert.Equal(t, 4, len(updated))
assert.Equal(t, 1, updated["db_id"])
assert.Equal(t, "01", updated["db_name"])
assert.Equal(t, "test", updated["title"])
assert.Equal(t, "Jakarta", updated["city_address"])

updated = DBUpdateTools{}.ToMap(
Model{ID: 1, Name: candihelper.ToStringPtr("01"), Address: "street", IgnoreMe: SubModel{Title: "t"}, SubModel: SubModel{Title: "test"}},
updated = DBUpdateTools{KeyExtractorFunc: DBUpdateMongoExtractorKey}.ToMap(
&Model{ID: 1, Name: candihelper.ToStringPtr("01"), Address: "street", SubModel: SubModel{Title: "test", CityAddress: "Jakarta"}, Rel: SubModel{Title: "rel sub"}},
)
assert.Equal(t, 6, len(updated))
assert.Equal(t, "street", updated["address"])
assert.Equal(t, 7, len(updated))
}

0 comments on commit 82109bb

Please sign in to comment.