Skip to content
This repository has been archived by the owner on Jun 25, 2024. It is now read-only.

Commit

Permalink
diff
Browse files Browse the repository at this point in the history
  • Loading branch information
vcraescu committed Nov 21, 2018
1 parent 8eec541 commit cb2c56b
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 22 deletions.
124 changes: 107 additions & 17 deletions callbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
50 changes: 50 additions & 0 deletions identity_manager.go
Original file line number Diff line number Diff line change
@@ -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[:])
}
20 changes: 16 additions & 4 deletions loggable.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
}
Expand All @@ -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:
Expand All @@ -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()
}
11 changes: 10 additions & 1 deletion options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package loggable

import "reflect"
import (
"reflect"
)

type Option func(options *options)

Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

0 comments on commit cb2c56b

Please sign in to comment.