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

Commit

Permalink
refactor everything (#5)
Browse files Browse the repository at this point in the history
* feat(all): refactor everything

* feat(readme.md): improve readme
  • Loading branch information
vetcher authored and sas1024 committed Mar 12, 2018
1 parent 762d902 commit 1b9c414
Show file tree
Hide file tree
Showing 8 changed files with 363 additions and 186 deletions.
58 changes: 55 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,58 @@ It creates `change_logs` table in your database and writes to all loggable model


## Usage
1. Register plugin using `loggable.Register(db)`
2. Add `loggable.LoggableModel` to your GORM model
3. If you want to set user, who makes changes, and place, where it happened, use `loggable.SetUserAndFrom("username", "London")`.
1. Register plugin using `loggable.Register(db)`.
```go
plugin, err := Register(database) // database is a *gorm.DB
if err != nil {
panic(err)
}
```
2. Add (embed) `loggable.LoggableModel` to your GORM model.
```go
type User struct{
Id uint
CreatedAt time.Time
// some other stuff...

loggable.LoggableModel
}
```
3. Changes after calling Create, Save, Update, Delete will be tracked.

## Customization
You may add additional fields to change logs, that should be saved.
First, embed `loggable.LoggableModel` to your model wrapper or directly to GORM model.
```go
type CreatedByLog struct {
// Public field will be catches by GORM and will be saved to main table.
CreatedBy string
// Hided field because we do not want to write this to main table,
// only to change_logs.
createdByPass string
loggable.LoggableModel
}
```
After that, shadow `LoggableModel`'s `Meta()` method by writing your realization, that should return structure with your information.
```go
type CreatedByLog struct {
CreatedBy string
createdByPass string
loggable.LoggableModel
}

func (m CreatedByLog) Meta() interface{} {
return struct { // You may define special type for this purposes, here we use unnamed one.
CreatedBy string
CreatedByPass string // CreatedByPass is a public because we want to track this field.
}{
CreatedBy: m.CreatedBy,
CreatedByPass: m.createdByPass,
}
}
```

## Options
#### 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.
146 changes: 25 additions & 121 deletions callbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,145 +2,49 @@ package loggable

import (
"encoding/json"
"reflect"
"sync"

"github.com/jinzhu/gorm"
"github.com/satori/go.uuid"
)

type LoggablePlugin interface {
// Deprecated: Use SetUserAndWhere instead.
SetUser(user string) *gorm.DB
// Deprecated: Use SetUserAndWhere instead.
SetWhere(from string) *gorm.DB
SetUserAndWhere(user, where string) *gorm.DB
GetRecords(objectId string) ([]*ChangeLog, error)
}

type Option func(options *options)

type plugin struct {
db *gorm.DB
mu sync.Mutex
opts options
}

func Register(db *gorm.DB, opts ...Option) (LoggablePlugin, error) {
err := db.AutoMigrate(&ChangeLog{}).Error
if err != nil {
return nil, err
}
o := options{}
for _, option := range opts {
option(&o)
}
r := &plugin{db: db, opts: o}
callback := db.Callback()
callback.Create().After("gorm:after_create").Register("loggable:create", r.addCreated)
callback.Update().After("gorm:after_update").Register("loggable:update", r.addUpdated)
callback.Delete().After("gorm:after_delete").Register("loggable:delete", r.addDeleted)
return r, nil
}

func (r *plugin) GetRecords(objectId string) ([]*ChangeLog, error) {
var changes []*ChangeLog
return changes, r.db.Where("object_id = ?", objectId).Find(&changes).Error
}

func (r *plugin) getLastRecord(objectId string) (*ChangeLog, error) {
var change ChangeLog
return &change, r.db.Where("object_id = ?", objectId).Order("created_at DESC").Limit(1).Find(&change).Error
}

// Deprecated: Use SetUserAndWhere instead.
func (r *plugin) SetUser(user string) *gorm.DB {
r.mu.Lock()
db := r.db.Set("loggable:user", user)
r.mu.Unlock()
return db
}

// Deprecated: Use SetUserAndWhere instead.
func (r *plugin) SetWhere(where string) *gorm.DB {
r.mu.Lock()
db := r.db.Set("loggable:where", where)
r.mu.Unlock()
return db
}

func (r *plugin) SetUserAndWhere(user, where string) *gorm.DB {
r.mu.Lock()
defer r.mu.Unlock()
return r.db.Set("loggable:user", user).Set("loggable:where", where)
}

func (r *plugin) addRecord(scope *gorm.Scope, action string) error {
var jsonObject JSONB
j, err := json.Marshal(scope.Value)
if err != nil {
return err
}
err = jsonObject.Scan(j)
if err != nil {
return err
}
user, where := getUserAndWhere(scope)

cl := ChangeLog{
ID: uuid.NewV4().String(),
ChangedBy: user.(string),
ChangedWhere: where.(string),
Action: action,
ObjectID: scope.PrimaryKeyValue().(string),
ObjectType: scope.GetModelStruct().ModelType.Name(),
Object: jsonObject,
}
return scope.DB().Create(&cl).Error
}

func getUserAndWhere(scope *gorm.Scope) (interface{}, interface{}) {
user, ok := scope.DB().Get("loggable:user")
if !ok {
user = ""
}
where, ok := scope.DB().Get("loggable:where")
if !ok {
where = ""
}
return user, where
}

func isLoggable(scope *gorm.Scope) (isLoggable bool) {
if scope.GetModelStruct().ModelType == nil {
return false
}
_, isLoggable = reflect.New(scope.GetModelStruct().ModelType).Interface().(loggableInterface)
return
}

func (r *plugin) addCreated(scope *gorm.Scope) {
func (p *Plugin) addCreated(scope *gorm.Scope) {
if isLoggable(scope) {
r.addRecord(scope, "create")
addRecord(scope, "create")
}
}

func (r *plugin) addUpdated(scope *gorm.Scope) {
func (p *Plugin) addUpdated(scope *gorm.Scope) {
if isLoggable(scope) {
if r.opts.lazyUpdate {
record, err := r.getLastRecord(scope.PrimaryKeyValue().(string))
if p.opts.lazyUpdate {
record, err := p.GetLastRecord(interfaceToString(scope.PrimaryKeyValue()), false)
if err == nil {
if isEqual(record.Object, scope.Value, r.opts.lazyUpdateFields...) {
if isEqual(record.RawObject, scope.Value, p.opts.lazyUpdateFields...) {
return
}
}
}
r.addRecord(scope, "update")
addRecord(scope, "update")
}
}

func (r *plugin) addDeleted(scope *gorm.Scope) {
func (p *Plugin) addDeleted(scope *gorm.Scope) {
if isLoggable(scope) {
r.addRecord(scope, "delete")
addRecord(scope, "delete")
}
}

func addRecord(scope *gorm.Scope, action string) error {
rawObject, err := json.Marshal(scope.Value)
if err != nil {
return err
}
cl := ChangeLog{
ID: uuid.NewV4().String(),
Action: action,
ObjectID: interfaceToString(scope.PrimaryKeyValue()),
ObjectType: scope.GetModelStruct().ModelType.Name(),
RawObject: rawObject,
RawMeta: fetchChangeLogMeta(scope),
}
return scope.DB().Create(&cl).Error
}
60 changes: 60 additions & 0 deletions jsonb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package loggable

import (
"bytes"
"database/sql/driver"
"encoding/json"
"errors"
"reflect"
)

type JSONB []byte

func (j JSONB) Value() (driver.Value, error) {
if j.IsNull() {
return nil, nil
}
return string(j), nil
}

func (j *JSONB) Scan(value interface{}) error {
if value == nil {
*j = nil
return nil
}
s, ok := value.([]byte)
if !ok {
return errors.New("scan source is not bytes")
}
*j = append((*j)[0:0], s...)
return nil
}

func (j JSONB) MarshalJSON() ([]byte, error) {
if j == nil {
return []byte("null"), nil
}
return j, nil
}

func (j *JSONB) UnmarshalJSON(data []byte) error {
if j == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
*j = append((*j)[0:0], data...)
return nil
}

func (j JSONB) IsNull() bool {
return len(j) == 0 || string(j) == "null"
}

func (j JSONB) Equals(j1 JSONB) bool {
return bytes.Equal([]byte(j), []byte(j1))
}

func (j JSONB) unmarshal(p reflect.Type) (interface{}, error) {
obj := reflect.New(p).Interface()
err := json.Unmarshal(j, obj)
return obj, err
}
Loading

0 comments on commit 1b9c414

Please sign in to comment.