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

Commit

Permalink
Merge pull request #14 from vcraescu/master
Browse files Browse the repository at this point in the history
  • Loading branch information
sas1024 authored Dec 11, 2018
2 parents 8eec541 + dfcb131 commit bb7f2fa
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 22 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,27 @@ func (m CreatedByLog) Meta() interface{} {
#### LazyUpdate
Option `LazyUpdate` allows save changes only if they big enough to be saved.
Plugin compares the last saved object and the new one, but ignores changes was made in fields from provided list.

#### ComputeDiff
Option `ComputeDiff` allows to only save the changes into the RawDiff field. This options is only relevant during update
operations. Only fields tagged with `gorm-loggable:true` will be taken in account. If the object does not have any field
tagged with `gorm-loggable:true` then the column will always be `NULL`.

e.g.

```go
type Person struct {
FirstName string `gorm-loggable:true`
LastName string `gorm-loggable:true`
Age int `gorm-loggable:true`
}
```

Let's say you change person `FirstName` from `John` to `Jack` and its `Age` from 30 to 40.
`ChangeLog.RawDiff` will be populated with the following:
```json
{
"FirstName": "Jack",
"Age": 40,
}
```
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
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/vcraescu/gorm-loggable

require (
github.com/gofrs/uuid v3.1.0+incompatible
github.com/jinzhu/copier v0.0.0-20180308034124-7e38e58719c3
github.com/jinzhu/gorm v1.9.2
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA=
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/jinzhu/copier v0.0.0-20180308034124-7e38e58719c3 h1:sHsPfNMAG70QAvKbddQ0uScZCHQoZsT5NykGRCeeeIs=
github.com/jinzhu/copier v0.0.0-20180308034124-7e38e58719c3/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw=
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
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:JSON"`
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 bb7f2fa

Please sign in to comment.