From cb2c56be7a01f37337c09a2ff18f22db663693e7 Mon Sep 17 00:00:00 2001 From: Viorel Craescu Date: Tue, 20 Nov 2018 17:28:23 +0200 Subject: [PATCH] diff --- callbacks.go | 124 ++++++++++++++++++++++++++++++++++++++------ identity_manager.go | 50 ++++++++++++++++++ loggable.go | 20 +++++-- options.go | 11 +++- plugin.go | 1 + util.go | 19 +++++++ 6 files changed, 203 insertions(+), 22 deletions(-) create mode 100644 identity_manager.go diff --git a/callbacks.go b/callbacks.go index 88974c1..3fbcd54 100644 --- a/callbacks.go +++ b/callbacks.go @@ -2,53 +2,143 @@ package loggable import ( "encoding/json" + "reflect" "github.com/gofrs/uuid" "github.com/jinzhu/gorm" ) +var im = newIdentityManager() + +const ( + actionCreate = "create" + actionUpdate = "update" + actionDelete = "delete" +) + +type UpdateDiff map[string]interface{} + +func (p *Plugin) trackEntity(scope *gorm.Scope) { + v := reflect.Indirect(reflect.ValueOf(scope.Value)) + + pkName := scope.PrimaryField().Name + if v.Kind() == reflect.Slice { + for i := 0; i < v.Len(); i++ { + sv := reflect.Indirect(v.Index(i)) + el := sv.Interface() + if !isLoggable(el) { + continue + } + + im.save(el, sv.FieldByName(pkName)) + } + return + } + + m := v.Interface() + if !isLoggable(m) { + return + } + + im.save(scope.Value, scope.PrimaryKeyValue()) +} + func (p *Plugin) addCreated(scope *gorm.Scope) { - if isLoggable(scope) && isEnabled(scope) { - addRecord(scope, "create") + if isLoggable(scope.Value) && isEnabled(scope.Value) { + addRecord(scope, actionCreate) } } func (p *Plugin) addUpdated(scope *gorm.Scope) { - if isLoggable(scope) && isEnabled(scope) { - if p.opts.lazyUpdate { - record, err := p.GetLastRecord(interfaceToString(scope.PrimaryKeyValue()), false) - if err == nil { - if isEqual(record.RawObject, scope.Value, p.opts.lazyUpdateFields...) { - return - } + if !isLoggable(scope.Value) || !isEnabled(scope.Value) { + return + } + + if p.opts.lazyUpdate { + record, err := p.GetLastRecord(interfaceToString(scope.PrimaryKeyValue()), false) + if err == nil { + if isEqual(record.RawObject, scope.Value, p.opts.lazyUpdateFields...) { + return } } - addRecord(scope, "update") } + + addUpdateRecord(scope, p.opts) } func (p *Plugin) addDeleted(scope *gorm.Scope) { - if isLoggable(scope) && isEnabled(scope) { - addRecord(scope, "delete") + if isLoggable(scope.Value) && isEnabled(scope.Value) { + addRecord(scope, actionDelete) } } -func addRecord(scope *gorm.Scope, action string) error { - rawObject, err := json.Marshal(scope.Value) +func addUpdateRecord(scope *gorm.Scope, opts options) error { + cl, err := newChangeLog(scope, actionUpdate) if err != nil { return err } + + if opts.computeDiff { + diff := computeUpdateDiff(scope) + jd, err := json.Marshal(diff) + if err != nil { + return err + } + + cl.RawDiff = string(jd) + } + + return scope.DB().Create(cl).Error +} + +func newChangeLog(scope *gorm.Scope, action string) (*ChangeLog, error) { + rawObject, err := json.Marshal(scope.Value) + if err != nil { + return nil, err + } id, err := uuid.NewV4() if err != nil { - return err + return nil, err } - cl := ChangeLog{ + + return &ChangeLog{ ID: id, Action: action, ObjectID: interfaceToString(scope.PrimaryKeyValue()), ObjectType: scope.GetModelStruct().ModelType.Name(), RawObject: string(rawObject), RawMeta: string(fetchChangeLogMeta(scope)), + }, nil +} + +func addRecord(scope *gorm.Scope, action string) error { + cl, err := newChangeLog(scope, action) + if err != nil { + return nil + } + + return scope.DB().Create(cl).Error +} + +func computeUpdateDiff(scope *gorm.Scope) UpdateDiff { + old := im.get(scope.Value, scope.PrimaryKeyValue()) + if old == nil { + return nil } - return scope.DB().Create(&cl).Error + + ov := reflect.ValueOf(old) + nv := reflect.Indirect(reflect.ValueOf(scope.Value)) + names := getLoggableFieldNames(old) + + diff := make(UpdateDiff) + + for _, name := range names { + ofv := ov.FieldByName(name).Interface() + nfv := nv.FieldByName(name).Interface() + if ofv != nfv { + diff[name] = nfv + } + } + + return diff } diff --git a/identity_manager.go b/identity_manager.go new file mode 100644 index 0000000..4ba945f --- /dev/null +++ b/identity_manager.go @@ -0,0 +1,50 @@ +package loggable + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "github.com/jinzhu/copier" + "reflect" +) + +type identityMap map[string]interface{} + +type identityManager struct { + m identityMap +} + +func newIdentityManager() *identityManager { + return &identityManager{ + m: make(identityMap), + } +} + +func (im *identityManager) save(value, pk interface{}) { + t := reflect.TypeOf(value) + newValue := reflect.New(t).Interface() + err := copier.Copy(&newValue, value) + if err != nil { + panic(err) + } + + im.m[genIdentityKey(t, pk)] = newValue +} + +func (im identityManager) get(value, pk interface{}) interface{} { + t := reflect.TypeOf(value) + key := genIdentityKey(t, pk) + m, ok := im.m[key] + if !ok { + return nil + } + + return m +} + +func genIdentityKey(t reflect.Type, pk interface{}) string { + key := fmt.Sprintf("%v_%s", pk, t.Name()) + b := md5.Sum([]byte(key)) + + return hex.EncodeToString(b[:]) +} diff --git a/loggable.go b/loggable.go index fc012ed..89048d7 100644 --- a/loggable.go +++ b/loggable.go @@ -42,6 +42,8 @@ type ChangeLog struct { ObjectType string `gorm:"index"` RawObject string `sql:"type:JSON"` RawMeta string `sql:"type:JSON"` + RawDiff string `sql:"type:JSONB"` + CreatedBy string `gorm:"index"` Object interface{} `sql:"-"` Meta interface{} `sql:"-"` } @@ -60,6 +62,16 @@ func (l *ChangeLog) prepareMeta(objType reflect.Type) (err error) { return } +func (l ChangeLog) Diff() (UpdateDiff, error) { + var diff UpdateDiff + err := json.Unmarshal([]byte(l.RawDiff), &diff) + if err != nil { + return nil, err + } + + return diff, nil +} + func interfaceToString(v interface{}) string { switch val := v.(type) { case string: @@ -81,12 +93,12 @@ func fetchChangeLogMeta(scope *gorm.Scope) []byte { return data } -func isLoggable(scope *gorm.Scope) bool { - _, ok := scope.Value.(Interface) +func isLoggable(value interface{}) bool { + _, ok := value.(Interface) return ok } -func isEnabled(scope *gorm.Scope) bool { - v, ok := scope.Value.(Interface) +func isEnabled(value interface{}) bool { + v, ok := value.(Interface) return ok && v.isEnabled() } diff --git a/options.go b/options.go index 018db03..d1f9c1e 100644 --- a/options.go +++ b/options.go @@ -1,6 +1,8 @@ package loggable -import "reflect" +import ( + "reflect" +) type Option func(options *options) @@ -9,6 +11,13 @@ type options struct { lazyUpdateFields []string metaTypes map[string]reflect.Type objectTypes map[string]reflect.Type + computeDiff bool +} + +func ComputeDiff() Option { + return func(options *options) { + options.computeDiff = true + } } func LazyUpdate(fields ...string) Option { diff --git a/plugin.go b/plugin.go index 98e98be..ab379fa 100644 --- a/plugin.go +++ b/plugin.go @@ -20,6 +20,7 @@ func Register(db *gorm.DB, opts ...Option) (Plugin, error) { } p := Plugin{db: db, opts: o} callback := db.Callback() + callback.Query().After("gorm:after_query").Register("loggable:query", p.trackEntity) callback.Create().After("gorm:after_create").Register("loggable:create", p.addCreated) callback.Update().After("gorm:after_update").Register("loggable:update", p.addUpdated) callback.Delete().After("gorm:after_delete").Register("loggable:delete", p.addDeleted) diff --git a/util.go b/util.go index 3e5ad5c..530b1a9 100644 --- a/util.go +++ b/util.go @@ -7,6 +7,8 @@ import ( "unicode" ) +const loggableTag = "gorm-loggable" + func isEqual(item1, item2 interface{}, except ...string) bool { except = StringMap(except, ToSnakeCase) m1, m2 := somethingToMapStringInterface(item1), somethingToMapStringInterface(item2) @@ -89,3 +91,20 @@ func isInStringSlice(what string, where []string) bool { } return false } + +func getLoggableFieldNames(value interface{}) []string { + var names []string + + t := reflect.TypeOf(value) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + value, ok := field.Tag.Lookup(loggableTag) + if !ok || value != "true" { + continue + } + + names = append(names, field.Name) + } + + return names +}