From 508dddd3048a0eed632ee26aa5dfc15f0dac9eb1 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Wed, 27 Jul 2022 20:16:44 +0200 Subject: [PATCH] Implement generic package for revisions --- pkg/revisions/model.go | 40 +++++++ pkg/revisions/operations.go | 14 +++ pkg/revisions/revisions.go | 225 ++++++++++++++++++++++++++++++++++++ pkg/revisions/service.go | 37 ++++++ 4 files changed, 316 insertions(+) create mode 100644 pkg/revisions/model.go create mode 100644 pkg/revisions/operations.go create mode 100644 pkg/revisions/revisions.go create mode 100644 pkg/revisions/service.go diff --git a/pkg/revisions/model.go b/pkg/revisions/model.go new file mode 100644 index 000000000..39faad5df --- /dev/null +++ b/pkg/revisions/model.go @@ -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, + }, + } +} diff --git a/pkg/revisions/operations.go b/pkg/revisions/operations.go new file mode 100644 index 000000000..bb83d1f3b --- /dev/null +++ b/pkg/revisions/operations.go @@ -0,0 +1,14 @@ +package revisions + +type ( + Operation = string +) + +const ( + unknown Operation = "" + Created = "created" + Updated = "updated" + SoftDeleted = "soft-deleted" + Restored = "restored" + HardDeleted = "hard-deleted" +) diff --git a/pkg/revisions/revisions.go b/pkg/revisions/revisions.go new file mode 100644 index 000000000..58d17daa8 --- /dev/null +++ b/pkg/revisions/revisions.go @@ -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 } diff --git a/pkg/revisions/service.go b/pkg/revisions/service.go new file mode 100644 index 000000000..bf84316f7 --- /dev/null +++ b/pkg/revisions/service.go @@ -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) + +}