Skip to content

Commit

Permalink
v2
Browse files Browse the repository at this point in the history
  • Loading branch information
vcraescu committed Nov 1, 2020
1 parent d56de03 commit c25c1ff
Show file tree
Hide file tree
Showing 10 changed files with 1,152 additions and 522 deletions.
89 changes: 55 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,86 @@
# gorm history [![Go Report Card](https://goreportcard.com/badge/github.com/vcraescu/gorm-history)](https://goreportcard.com/report/github.com/vcraescu/gorm-history) [![Build Status](https://travis-ci.com/vcraescu/gorm-history.svg?branch=master)](https://travis-ci.com/vcraescu/gorm-history) [![Coverage Status](https://coveralls.io/repos/github/vcraescu/gorm-history/badge.svg?branch=master)](https://coveralls.io/github/vcraescu/gorm-history?branch=master) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
History is used to keep a history record of your GORM models.
Each model must be associated with an history model which is a copy of the modified record.
You can use the plugin to keep a history of your GORM models changes. Basically it keeps a ledger of your model state changes.
Each model must be associated with a history model which is a copy of the modified record.

## Install
```
go get github.com/vcraescu/gorm-history
```

## Usage
1. Register the plugin using `history.Register(db)`:

```go
plugin, err := Register(db) // db is a *gorm.DB
if err != nil {
panic(err)
}
```
1. Register the plugin using `db.Use(history.New())`:

2. Define your model and history model:
```go
type Person struct {
gorm.Model
FirstName string
LastName string
Address *string
gorm.Model

FirstName string
LastName string
}

type PersonHistory struct {
gorm.Model
history.Entry
gorm.Model
Entry
}

FirstName string
LastName string
Address *string
func main() {
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
panic(err)
}

db = db.Session(&gorm.Session{})

err = db.AutoMigrate(Person{}, PersonHistory{})
if err != nil {
panic(err)
}

plugin := New()
if err := db.Use(plugin); err != nil {
return
}

db = SetUser(db, history.User{
ID: 123,
Email: "john@doe.com",
})
db = SetSource(db, history.Source{
ID: "1c059a03-3b14-4017-ae33-5337860ec35f",
Type: "ampq",
})

p := Person{
FirstName: "John",
LastName: "Doe",
}
if err := db.Save(&p).Error; err != nil {
panic(err)
}
}
```

3. Your model must implement `history.Recordable` interface:
2. Your model must implement `history.Recordable` interface:
```go
func (Person) CreateHistory() interface{} {
return PersonHistory{}
}
```

4. Changes after calling Create, Save, Update will be tracked.

3. Changes after calling Create, Save and Update will be recorded as long as you pass in the original object.

*Note*: By default, the plugin will use time based versioning function for your history record which might not be 100%
accurate in some cases but it is faster because it doesn't require to query the db history table at all.
```go
if err := db.Model(&p).Update("first_name", "Jane").Error; err != nil {
panic(err)
}
```

## Configuration

### Versioning

* `history.TimedVersionFunc` - It returns the nanonseconds value when the create/update happens;
* `history.IncrementedVersionFunc` - It returns the maximum + 1 of current version column. If you use this versioning
method than you must tag your history model fields (or embed `history.Entry` struct) like this instead of
implementing the `history.History` interface:
By default, the plugin generates a new ULID for each history record. You can implement your own versioning function.

```go
type PersonHistory struct {
Expand All @@ -77,8 +101,7 @@ type PersonHistory struct {

You can change the versioning function when you register the plugin:
```go
plugin, err := Register(db, history.WithVersionFunc(history.IncrementedVersionFunc)) // db is a *gorm.DB
if err != nil {
if err := db.Use(history.New(history.WithVersionFunc(MyVersionFunc))); err != nil {
panic(err)
}
```
Expand All @@ -87,17 +110,15 @@ if err != nil {

* `history.DefaultCopyFunc` - copies all the values of the recordable model to history model.

You can change the copy function when you register the plugin if you defined your own function:
You can change the copy function when you register the plugin if you defined your own copying function:

```go
func myCopyFunc(r Recordable, history interface{}) error {
// ...
}

//...

plugin, err := Register(db, history.WithCopyFunc(myCopyFunc)) // db is a *gorm.DB
if err != nil {
if err := db.Use(history.New(history.WithCopyFunc(myCopyFunc))); err != nil {
panic(err)
}
```
Expand Down
8 changes: 0 additions & 8 deletions export_test.go

This file was deleted.

5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ module github.com/vcraescu/gorm-history
go 1.13

require (
github.com/davecgh/go-spew v1.1.1
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/jinzhu/gorm v1.9.12
github.com/oklog/ulid/v2 v2.0.2
github.com/stretchr/objx v0.2.0 // indirect
github.com/stretchr/testify v1.5.1
gopkg.in/yaml.v2 v2.2.8 // indirect
gorm.io/driver/sqlite v1.1.3
gorm.io/gorm v1.20.1
)
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
Expand All @@ -48,3 +56,7 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
gorm.io/gorm v1.20.1 h1:+hOwlHDqvqmBIMflemMVPLJH7tZYK4RxFDBHEfJTup0=
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
152 changes: 132 additions & 20 deletions history.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,149 @@
package history

type Action string
import (
"context"
"fmt"
"gorm.io/gorm"
"time"
)

const (
ActionCreate Action = "create"
ActionUpdate Action = "update"
ActionDelete Action = "delete"
ActionCreate Action = "create"
ActionUpdate Action = "update"
userOptionKey userOptionCtxKey = pluginName + ":user"
sourceOptionKey sourceOptionCtxKey = pluginName + ":source"
)

var (
_ History = (*Entry)(nil)
_ TimestampableHistory = (*Entry)(nil)
_ BlameableHistory = (*Entry)(nil)
_ SourceableHistory = (*Entry)(nil)
)

type (
Version string

Action string

userOptionCtxKey string

sourceOptionCtxKey string

Recordable interface {
CreateHistory() History
}

TimestampableHistory interface {
SetHistoryCreatedAt(createdAt time.Time)
}

BlameableHistory interface {
SetHistoryUserID(id string)
SetHistoryUserEmail(email string)
}

SourceableHistory interface {
SetHistorySourceID(ID string)
SetHistorySourceType(typ string)
}

History interface {
SetHistoryVersion(version Version)
SetHistoryObjectID(id interface{})
SetHistoryAction(action Action)
}

Entry struct {
Version Version `gorm:"type:char(26)"`
ObjectID string `gorm:"index"`
Action Action `gorm:"type:varchar(24)"`
UserID string `gorm:"type:varchar(255)"`
UserEmail string `gorm:"type:varchar(255)"`
SourceID string `gorm:"type:varchar(255)"`
SourceType string `gorm:"type:varchar(255)"`
CreatedAt time.Time `gorm:"type:datetime"`
}

User struct {
ID string
Email string
}

Source struct {
ID string
Type string
}
)

type Recordable interface {
CreateHistory() interface{}
func SetUser(db *gorm.DB, user User) *gorm.DB {
ctx := context.WithValue(db.Statement.Context, userOptionKey, user)

return db.WithContext(ctx).Set(string(userOptionKey), user)
}

func GetUser(db *gorm.DB) (User, bool) {
value, ok := db.Get(string(userOptionKey))
if !ok {
value := db.Statement.Context.Value(userOptionKey)
user, ok := value.(User)

return user, ok
}

user, ok := value.(User)

return user, ok
}

func SetSource(db *gorm.DB, source Source) *gorm.DB {
ctx := context.WithValue(db.Statement.Context, sourceOptionKey, source)

return db.WithContext(ctx).Set(string(sourceOptionKey), source)
}

func GetSource(db *gorm.DB) (Source, bool) {
value, ok := db.Get(string(sourceOptionKey))
if !ok {
value := db.Statement.Context.Value(sourceOptionKey)
source, ok := value.(Source)

return source, ok
}

source, ok := value.(Source)

return source, ok
}

func (e *Entry) SetHistoryVersion(version Version) {
e.Version = version
}

func (e *Entry) SetHistoryObjectID(id interface{}) {
e.ObjectID = fmt.Sprintf("%v", id)
}

func (e *Entry) SetHistoryAction(action Action) {
e.Action = action
}

type History interface {
SetHistoryVersion(version Version)
SetHistoryObjectID(id interface{})
SetHistoryAction(action Action)
func (e *Entry) SetHistoryUserID(id string) {
e.UserID = id
}

type Entry struct {
Version Version `gorm-history:"version"`
ObjectID uint `gorm:"index" gorm-history:"objectID"`
Action Action `gorm:"type: string" gorm-history:"action"`
func (e *Entry) SetHistoryUserEmail(email string) {
e.UserEmail = email
}

func (l *Entry) SetHistoryVersion(version Version) {
l.Version = version
func (e *Entry) SetHistoryCreatedAt(createdAt time.Time) {
e.CreatedAt = createdAt
}

func (l *Entry) SetHistoryObjectID(id interface{}) {
l.ObjectID = id.(uint)
func (e *Entry) SetHistorySourceID(id string) {
e.SourceID = id
}

func (l *Entry) SetHistoryAction(action Action) {
l.Action = action
func (e *Entry) SetHistorySourceType(typ string) {
e.SourceType = typ
}

0 comments on commit c25c1ff

Please sign in to comment.