Implement generic package for revisions

This commit is contained in:
Denis Arh
2022-07-31 16:29:28 +02:00
parent 04e93ede90
commit 508dddd304
4 changed files with 316 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
package revisions
import (
"github.com/cortezaproject/corteza-server/pkg/dal"
"github.com/cortezaproject/corteza-server/pkg/dal/capabilities"
"github.com/cortezaproject/corteza-server/pkg/id"
)
const (
RevisionResourceType = "corteza::system:revision"
)
// Model returns generic dal.Model for storing revisions
//
// Returns only basic
func Model() *dal.Model {
// make revision model
return &dal.Model{
ResourceID: id.Next(),
Ident: "revisions",
ResourceType: RevisionResourceType,
Attributes: dal.AttributeSet{
&dal.Attribute{Ident: "id", PrimaryKey: true, Store: &dal.CodecPlain{}, Type: &dal.TypeID{}},
&dal.Attribute{Ident: "ts", Store: &dal.CodecPlain{}, Type: &dal.TypeTimestamp{}},
&dal.Attribute{Ident: "revision", Store: &dal.CodecPlain{}, Type: &dal.TypeNumber{}},
&dal.Attribute{Ident: "operation", Store: &dal.CodecPlain{}, Type: &dal.TypeNumber{}},
&dal.Attribute{Ident: "rel_resource", Store: &dal.CodecPlain{}, Type: &dal.TypeID{}},
&dal.Attribute{Ident: "rel_user", Store: &dal.CodecPlain{}, Type: &dal.TypeID{}},
&dal.Attribute{Ident: "delta", Store: &dal.CodecPlain{}, Type: &dal.TypeJSON{}},
&dal.Attribute{Ident: "comment", Store: &dal.CodecPlain{}, Type: &dal.TypeText{}},
},
Capabilities: capabilities.Set{
capabilities.Create,
capabilities.Search,
},
}
}
+14
View File
@@ -0,0 +1,14 @@
package revisions
type (
Operation = string
)
const (
unknown Operation = ""
Created = "created"
Updated = "updated"
SoftDeleted = "soft-deleted"
Restored = "restored"
HardDeleted = "hard-deleted"
)
+225
View File
@@ -0,0 +1,225 @@
package revisions
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/cast2"
"github.com/cortezaproject/corteza-server/pkg/dal"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/id"
"time"
)
type (
// generic revision struct
Revision struct {
ID uint64 `json:"changeID,string"`
Timestamp time.Time `json:"timestamp"`
ResourceID uint64 `json:"resourceID,string"`
Revision uint `json:"revision"`
Operation Operation `json:"event"`
UserID uint64 `json:"userID,string"`
Changes []*Change `json:"changes"`
Comment string `json:"comment"`
}
Change struct {
// changed field
Key string `json:"key"`
Old []any `json:"old,omitempty"`
New []any `json:"new,omitempty"`
}
Filter struct {
ResourceID uint64 `json:"resourceID,string"`
}
)
var (
now = func() time.Time {
return time.Now().Round(time.Second)
}
)
func Make(op Operation, revision uint, resourceID, userID uint64) (rev *Revision) {
return &Revision{
ID: id.Next(),
Timestamp: now(),
ResourceID: resourceID,
Revision: revision,
UserID: userID,
Operation: op,
}
}
// untested
func (r *Revision) CollectChanges(new, old dal.ValueGetter, omit ...string) (err error) {
var (
cc = make([]*Change, 0)
ch *Change
// old values count
ovc map[string]uint
val any
)
if new == nil {
return
}
if old != nil {
// old values count
ovc = old.CountValues()
}
keys:
for key, count := range new.CountValues() {
if count == 0 && old == nil {
continue keys
}
for _, o := range omit {
if key == o {
continue keys
}
}
ch = &Change{
Key: key,
New: make([]any, 0, count),
Old: make([]any, 0, count),
}
for pos := uint(0); pos < count; pos++ {
val, err = new.GetValue(key, pos)
if err != nil {
return
}
ch.New = append(ch.New, val)
}
if old == nil {
cc = append(cc, ch)
continue
}
for pos := uint(0); pos < ovc[key]; pos++ {
val, err = old.GetValue(key, pos)
if err != nil {
return
}
ch.Old = append(ch.Old, val)
}
if len(ch.New) != len(ch.Old) {
// different sizes, means that something has changed
cc = append(cc, ch)
continue
}
for i := range ch.New {
// go over all and append on first found difference
if ch.New[i] != ch.Old[i] {
cc = append(cc, ch)
break
}
}
}
r.Changes = cc
return
}
// CountValues satisfies dal.ValueGetter interface
func (r *Revision) CountValues() map[string]uint {
// signaling DAL that each attribute has exactly one value!
return nil
}
// GetValue satisfies dal.ValueGetter interface
func (r *Revision) GetValue(ident string, _ uint) (any, error) {
switch ident {
case "id":
return r.ID, nil
case "ts":
return r.Timestamp, nil
case "rel_resource":
return r.ResourceID, nil
case "revision":
return r.Revision, nil
case "operation":
return r.Operation, nil
case "rel_user":
return r.UserID, nil
case "delta":
return r.Changes, nil
case "comment":
return r.Comment, nil
}
return nil, nil
}
func (r *Revision) SetValue(name string, _ uint, value any) error {
switch name {
case "id":
return cast2.Uint64(value, &r.ID)
case "ts":
return cast2.Time(value, &r.Timestamp)
case "rel_resource":
return cast2.Uint64(value, &r.ResourceID)
case "revision":
return cast2.Uint(value, &r.Revision)
case "operation":
return cast2.String(value, &r.Operation)
case "rel_user":
return cast2.Uint64(value, &r.UserID)
case "delta":
if bb, is := value.([]byte); is {
return json.Unmarshal(bb, &r.Changes)
}
return fmt.Errorf("unexpected type for delta: %T", value)
case "comment":
return cast2.String(value, &r.Comment)
}
return nil
}
func (f Filter) Constraints() map[string][]any {
return map[string][]any{
"rel_resource": {f.ResourceID},
}
}
func (f Filter) Expression() string { return "" }
func (f Filter) OrderBy() filter.SortExprSet { return nil }
func (f Filter) Limit() uint { return 0 }
func (f Filter) Cursor() *filter.PagingCursor { return nil }
func (f Filter) StateConstraints() map[string]filter.State { return nil }
+37
View File
@@ -0,0 +1,37 @@
package revisions
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/dal"
"github.com/cortezaproject/corteza-server/pkg/dal/capabilities"
"github.com/cortezaproject/corteza-server/pkg/filter"
)
type (
Servicer interface {
Search(ctx context.Context, mf dal.ModelRef, f filter.Filter) (_ dal.Iterator, err error)
Create(ctx context.Context, mf dal.ModelRef, revision *Revision) error
}
creatorSearcher interface {
Search(ctx context.Context, m dal.ModelRef, capabilities capabilities.Set, f filter.Filter) (dal.Iterator, error)
Create(ctx context.Context, m dal.ModelRef, capabilities capabilities.Set, vv ...dal.ValueGetter) error
}
service struct {
dal creatorSearcher
}
)
func Service(dal creatorSearcher) *service {
return &service{dal: dal}
}
func (svc *service) Search(ctx context.Context, mf dal.ModelRef, f filter.Filter) (_ dal.Iterator, err error) {
return svc.dal.Search(ctx, mf, capabilities.Set{capabilities.Search}, f)
}
func (svc *service) Create(ctx context.Context, mf dal.ModelRef, revision *Revision) error {
return svc.dal.Create(ctx, mf, capabilities.Set{capabilities.Create}, revision)
}