3
0

Post testing tweaks and fixes

This commit is contained in:
Tomaž Jerman
2023-06-20 14:14:54 +02:00
parent 7b5e8d1aa9
commit 6795e5e0f2
15 changed files with 1225 additions and 157 deletions
+5 -8
View File
@@ -23,7 +23,6 @@ import (
apigwTypes "github.com/cortezaproject/corteza/server/pkg/apigw/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/corredor"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/healthcheck"
"github.com/cortezaproject/corteza/server/pkg/http"
@@ -385,9 +384,6 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) {
return
}
// Add alteration management service the global DAL service
dal.Service().Alterations = service.DefaultDalSchemaAlteration
if app.Opt.Messagebus.Enabled {
// initialize all the queue handlers
messagebus.Service().Init(ctx, service.DefaultQueue)
@@ -412,10 +408,11 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) {
// Note: this is a legacy approach, all services from all 3 apps
// will most likely be merged in the future
err = cmpService.Initialize(ctx, app.Log, app.Store, cmpService.Config{
ActionLog: app.Opt.ActionLog,
Discovery: app.Opt.Discovery,
Storage: app.Opt.ObjStore,
UserFinder: sysService.DefaultUser,
ActionLog: app.Opt.ActionLog,
Discovery: app.Opt.Discovery,
Storage: app.Opt.ObjStore,
UserFinder: sysService.DefaultUser,
SchemaAltManager: sysService.DefaultDalSchemaAlteration,
})
if err != nil {
+2 -1
View File
@@ -189,7 +189,8 @@ func (e StoreEncoder) encodeModuleExtend(ctx context.Context, p envoyx.EncodePar
}
// @note there is only one model so this is ok
return dl.ReplaceModel(ctx, models[0])
_, err = dl.ReplaceModel(ctx, nil, models[0])
return
}
func (e StoreEncoder) postModulesEncode(ctx context.Context, p envoyx.EncodeParams, s store.Storer, tree envoyx.Traverser, nn envoyx.NodeSet) (err error) {
+31 -17
View File
@@ -40,7 +40,8 @@ type (
store store.Storer
locale ResourceTranslationsManagerService
dal dalModelManager
dal dalModelManager
schemaAltManager schemaAltManager
}
moduleAccessController interface {
@@ -78,7 +79,7 @@ type (
GetConnectionByID(ID uint64) *dal.ConnectionWrap
Search(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (dal.Iterator, error)
ReplaceModel(context.Context, *dal.Model) error
ReplaceModel(context.Context, []*dal.Alteration, *dal.Model) (newAlts []*dal.Alteration, err error)
RemoveModel(ctx context.Context, connectionID, ID uint64) error
SearchModelIssues(ID uint64) []dal.Issue
}
@@ -145,14 +146,15 @@ var (
})
)
func Module() *module {
func Module(am schemaAltManager) *module {
return &module{
ac: DefaultAccessControl,
eventbus: eventbus.Service(),
actionlog: DefaultActionlog,
store: DefaultStore,
locale: DefaultResourceTranslation,
dal: dal.Service(),
ac: DefaultAccessControl,
eventbus: eventbus.Service(),
actionlog: DefaultActionlog,
store: DefaultStore,
locale: DefaultResourceTranslation,
dal: dal.Service(),
schemaAltManager: am,
}
}
@@ -405,7 +407,7 @@ func (svc module) Create(ctx context.Context, new *types.Module) (*types.Module,
return
}
if err = DalModelReplace(ctx, svc.dal, ns, new); err != nil {
if err = DalModelReplace(ctx, s, svc.schemaAltManager, svc.dal, ns, new); err != nil {
return err
}
@@ -434,7 +436,7 @@ func (svc module) UndeleteByID(ctx context.Context, namespaceID, moduleID uint64
//
// Directly using store so we don't spam the action log
func (svc *module) ReloadDALModels(ctx context.Context) (err error) {
return DalModelReload(ctx, svc.store, svc.dal)
return DalModelReload(ctx, svc.store, svc.schemaAltManager, svc.dal)
}
// SearchSensitive will list all module with at least one private module field
@@ -619,7 +621,7 @@ func (svc module) updater(ctx context.Context, namespaceID, moduleID uint64, act
if err = svc.eventbus.WaitFor(ctx, event.ModuleAfterUpdate(m, old, ns)); err != nil {
return err
}
if err = DalModelReplace(ctx, svc.dal, ns, old, m); err != nil {
if err = DalModelReplace(ctx, s, svc.schemaAltManager, svc.dal, ns, old, m); err != nil {
return err
}
} else {
@@ -1092,7 +1094,7 @@ func loadModuleLabels(ctx context.Context, s store.Labels, set ...*types.Module)
}
// DalModelReload reloads all defined compose modules into the DAL
func DalModelReload(ctx context.Context, s store.Storer, dmm dalModelManager) (err error) {
func DalModelReload(ctx context.Context, s store.Storer, am schemaAltManager, dmm dalModelManager) (err error) {
// Get all available namespaces
nn, _, err := store.SearchComposeNamespaces(ctx, s, types.NamespaceFilter{})
if err != nil {
@@ -1112,7 +1114,7 @@ func DalModelReload(ctx context.Context, s store.Storer, dmm dalModelManager) (e
// Reload!
for _, ns := range nn {
err = DalModelReplace(ctx, dmm, ns, modulesForNamespace(ns, mm)...)
err = DalModelReplace(ctx, s, am, dmm, ns, modulesForNamespace(ns, mm)...)
if err != nil {
return
}
@@ -1135,9 +1137,11 @@ func modulesForNamespace(ns *types.Namespace, mm types.ModuleSet) (out types.Mod
}
// Replaces all given connections
func DalModelReplace(ctx context.Context, dmm dalModelManager, ns *types.Namespace, modules ...*types.Module) (err error) {
func DalModelReplace(ctx context.Context, s store.Storer, am schemaAltManager, dmm dalModelManager, ns *types.Namespace, modules ...*types.Module) (err error) {
var (
models dal.ModelSet
models dal.ModelSet
currentAlts []*dal.Alteration
newAlts []*dal.Alteration
)
models, err = ModulesToModelSet(dmm, ns, modules...)
@@ -1146,7 +1150,17 @@ func DalModelReplace(ctx context.Context, dmm dalModelManager, ns *types.Namespa
}
for _, m := range models {
err = dmm.ReplaceModel(ctx, m)
currentAlts, err = am.ModelAlterations(ctx, m)
if err != nil {
return
}
newAlts, err = dmm.ReplaceModel(ctx, currentAlts, m)
if err != nil {
return
}
err = am.SetAlterations(ctx, s, m, currentAlts, newAlts...)
if err != nil {
return
}
+11 -5
View File
@@ -33,11 +33,17 @@ type (
FindByID(context.Context, uint64) (*systemTypes.User, error)
}
schemaAltManager interface {
ModelAlterations(context.Context, *dal.Model) (out []*dal.Alteration, err error)
SetAlterations(ctx context.Context, s store.Storer, m *dal.Model, stale []*dal.Alteration, set ...*dal.Alteration) (err error)
}
Config struct {
ActionLog options.ActionLogOpt
Discovery options.DiscoveryOpt
Storage options.ObjectStoreOpt
UserFinder userFinder
ActionLog options.ActionLogOpt
Discovery options.DiscoveryOpt
Storage options.ObjectStoreOpt
UserFinder userFinder
SchemaAltManager schemaAltManager
}
eventDispatcher interface {
@@ -176,7 +182,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config)
}
DefaultNamespace = Namespace()
DefaultModule = Module()
DefaultModule = Module(c.SchemaAltManager)
DefaultImportSession = ImportSession()
DefaultRecord = Record()
+194 -10
View File
@@ -1,6 +1,9 @@
package dal
import "encoding/json"
import (
"encoding/json"
"fmt"
)
type (
Alteration struct {
@@ -36,7 +39,7 @@ type (
AttributeReEncode struct {
Attr *Attribute `json:"attr"`
To *Attribute `json:"to"`
To Codec `json:"to"`
}
ModelAdd struct {
@@ -52,24 +55,69 @@ type (
// This is required since the Type inside the Attribute is an interface and we
// need to help the encoding/json a bit.
auxAttributeReType struct {
Attr *Attribute
To *auxAttributeType
Attr *Attribute `json:"attr"`
To *auxAttributeType `json:"to"`
}
// auxAttributeReEncode is a helper struct used for marshaling/unmarshaling
//
// This is required since the Codec inside the Attribute is an interface and we
// need to help the encoding/json a bit.
auxAttributeReEncode struct {
Attr *Attribute `json:"attr"`
To *auxStoreCodec `json:"to"`
}
)
// Merge merges the two alteration slices
func (a AlterationSet) Merge(b AlterationSet) (c AlterationSet) {
// @todo don't blindly append the two slices since there can be duplicates
// or overlapping alterations which would cause needles processing
func (aa AlterationSet) Merge(bb AlterationSet) (cc AlterationSet) {
// @todo the Merge function currently just merges the two slices together
// and removes any duplicates that would occur due to the merge.
// We should also handle overlapping/transitive alterations to reduce
// the amount of needles processing.
//
// A quick list of overlapping alterations:
// * attribute A added and then renamed from A to A'
// * attribute A renamed to A' and then renamed to A''
// * attribute A deleted and then created
//
// For now we'll simply append them and worry about improvements on a later stage
return append(a, b...)
cc = make(AlterationSet, 0, len(aa)+len(bb))
skip := make(map[int]bool, (len(aa)+len(bb))/2)
// For each item in aa, check if it has a matching element in bb.
// If it does, mark the bb index as skipped, if it doesn't use the aa element.
// If a duplicate is found, the bb element is used (considered newer),
//
// This is sub-optimal but the slices are expected to be small and this
// won't be ran often.
for _, a := range aa {
found := false
for j, b := range bb {
if skip[j] {
continue
}
if a.compare(*b) {
skip[j] = true
cc = append(cc, b)
found = true
break
}
}
if !found {
cc = append(cc, a)
}
}
for j, b := range bb {
if skip[j] {
continue
}
cc = append(cc, b)
}
return
}
func (a AttributeReType) MarshalJSON() ([]byte, error) {
@@ -191,3 +239,139 @@ func (a *AttributeReType) UnmarshalJSON(data []byte) (err error) {
return
}
func (a AttributeReEncode) MarshalJSON() ([]byte, error) {
aux := auxAttributeReEncode{
Attr: a.Attr,
To: &auxStoreCodec{},
}
switch t := a.To.(type) {
case *CodecPlain:
aux.To.Type = "CodecPlain"
aux.To.CodecPlain = t
case *CodecRecordValueSetJSON:
aux.To.Type = "CodecRecordValueSetJSON"
aux.To.CodecRecordValueSetJSON = t
case *CodecAlias:
aux.To.Type = "CodecAlias"
aux.To.CodecAlias = t
}
return json.Marshal(aux)
}
func (a *AttributeReEncode) UnmarshalJSON(data []byte) (err error) {
aux := &auxAttributeReEncode{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
if a == nil {
*a = AttributeReEncode{}
}
a.Attr = aux.Attr
switch aux.To.Type {
case "CodecPlain":
a.To = aux.To.CodecPlain
case "CodecRecordValueSetJSON":
a.To = aux.To.CodecRecordValueSetJSON
case "CodecAlias":
a.To = aux.To.CodecAlias
}
return
}
func (a Alteration) compare(b Alteration) (cmp bool) {
if a.AttributeAdd == nil && b.AttributeAdd != nil {
return false
}
if a.AttributeDelete == nil && b.AttributeDelete != nil {
return false
}
if a.AttributeReType == nil && b.AttributeReType != nil {
return false
}
if a.AttributeReEncode == nil && b.AttributeReEncode != nil {
return false
}
if a.ModelAdd == nil && b.ModelAdd != nil {
return false
}
if a.ModelDelete == nil && b.ModelDelete != nil {
return false
}
switch {
case a.AttributeAdd != nil:
return a.compareAttributeAdd(b)
case a.AttributeDelete != nil:
return a.compareAttributeDelete(b)
case a.AttributeReType != nil:
return a.compareAttributeReType(b)
case a.AttributeReEncode != nil:
return a.compareAttributeReEncode(b)
case a.ModelAdd != nil:
return a.compareModelAdd(b)
case a.ModelDelete != nil:
return a.compareModelDelete(b)
}
panic(fmt.Sprintf("unsupported alteration type %v", a))
}
func (a Alteration) compareAttributeAdd(b Alteration) bool {
if a.AttributeAdd == nil || b.AttributeAdd == nil {
return a.AttributeAdd == b.AttributeAdd
}
return a.AttributeAdd.Attr.Compare(b.AttributeAdd.Attr)
}
func (a Alteration) compareAttributeDelete(b Alteration) bool {
if a.AttributeDelete == nil || b.AttributeDelete == nil {
return a.AttributeDelete == b.AttributeDelete
}
return a.AttributeDelete.Attr.Compare(b.AttributeDelete.Attr)
}
func (a Alteration) compareAttributeReType(b Alteration) bool {
if !a.AttributeReType.Attr.Compare(b.AttributeReType.Attr) {
return false
}
if a.AttributeReType == nil || b.AttributeReType == nil {
return a.AttributeReType == b.AttributeReType
}
return a.AttributeReType.To.Type() == b.AttributeReType.To.Type()
}
func (a Alteration) compareAttributeReEncode(b Alteration) bool {
if !a.AttributeReEncode.Attr.Compare(b.AttributeReEncode.Attr) {
return false
}
if a.AttributeReEncode == nil || b.AttributeReEncode == nil {
return a.AttributeReEncode == b.AttributeReEncode
}
return a.AttributeReEncode.To.Type() == b.AttributeReEncode.To.Type()
}
func (a Alteration) compareModelAdd(b Alteration) bool {
if a.ModelAdd == nil || b.ModelAdd == nil {
return a.ModelAdd == b.ModelAdd
}
return a.ModelAdd.Model.Compare(*b.ModelAdd.Model)
}
func (a Alteration) compareModelDelete(b Alteration) bool {
if a.ModelDelete == nil || b.ModelDelete == nil {
return a.ModelDelete == b.ModelDelete
}
return a.ModelDelete.Model.Compare(*b.ModelDelete.Model)
}
+560
View File
@@ -0,0 +1,560 @@
package dal
import (
"encoding/json"
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
func TestMerge(t *testing.T) {
tcc := []struct {
name string
aa AlterationSet
bb AlterationSet
cc AlterationSet
}{
{
name: "empty",
aa: AlterationSet{},
bb: AlterationSet{},
cc: AlterationSet{},
},
{
name: "aa empty",
aa: AlterationSet{},
bb: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
cc: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
},
{
name: "bb empty",
aa: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
bb: AlterationSet{},
cc: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
},
{
name: "un matching types",
aa: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
bb: AlterationSet{
&Alteration{
AttributeDelete: &AttributeDelete{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
cc: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
&Alteration{
AttributeDelete: &AttributeDelete{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
},
{
name: "match AttributeAdd",
aa: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
bb: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
cc: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
},
{
name: "not match AttributeAdd",
aa: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
bb: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "bar",
Type: &TypeJSON{Nullable: false},
},
},
},
},
cc: AlterationSet{
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: "bar",
Type: &TypeJSON{Nullable: false},
},
},
},
},
},
{
name: "match AttributeDelete",
aa: AlterationSet{
&Alteration{
AttributeDelete: &AttributeDelete{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
bb: AlterationSet{
&Alteration{
AttributeDelete: &AttributeDelete{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
cc: AlterationSet{
&Alteration{
AttributeDelete: &AttributeDelete{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
},
{
name: "not match AttributeDelete",
aa: AlterationSet{
&Alteration{
AttributeDelete: &AttributeDelete{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
},
bb: AlterationSet{
&Alteration{
AttributeDelete: &AttributeDelete{
Attr: &Attribute{
Ident: "bar",
Type: &TypeJSON{Nullable: false},
},
},
},
},
cc: AlterationSet{
&Alteration{
AttributeDelete: &AttributeDelete{
Attr: &Attribute{
Ident: "foo",
Type: &TypeJSON{Nullable: false},
},
},
},
&Alteration{
AttributeDelete: &AttributeDelete{
Attr: &Attribute{
Ident: "bar",
Type: &TypeJSON{Nullable: false},
},
},
},
},
},
{
name: "match AttributeReType",
aa: AlterationSet{
&Alteration{
AttributeReType: &AttributeReType{
Attr: &Attribute{
Ident: "foo",
},
To: &TypeBoolean{Nullable: false},
},
},
},
bb: AlterationSet{
&Alteration{
AttributeReType: &AttributeReType{
Attr: &Attribute{
Ident: "foo",
},
To: &TypeBoolean{Nullable: false},
},
},
},
cc: AlterationSet{
&Alteration{
AttributeReType: &AttributeReType{
Attr: &Attribute{
Ident: "foo",
},
To: &TypeBoolean{Nullable: false},
},
},
},
},
{
name: "not match AttributeReType",
aa: AlterationSet{
&Alteration{
AttributeReType: &AttributeReType{
Attr: &Attribute{
Ident: "foo",
},
To: &TypeBoolean{Nullable: false},
},
},
},
bb: AlterationSet{
&Alteration{
AttributeReType: &AttributeReType{
Attr: &Attribute{
Ident: "foo",
},
To: &TypeText{Nullable: false},
},
},
},
cc: AlterationSet{
&Alteration{
AttributeReType: &AttributeReType{
Attr: &Attribute{
Ident: "foo",
},
To: &TypeBoolean{Nullable: false},
},
},
&Alteration{
AttributeReType: &AttributeReType{
Attr: &Attribute{
Ident: "foo",
},
To: &TypeText{Nullable: false},
},
},
},
},
{
name: "match AttributeReEncode",
aa: AlterationSet{
&Alteration{
AttributeReEncode: &AttributeReEncode{
Attr: &Attribute{Ident: "foo"},
To: &CodecPlain{},
},
},
},
bb: AlterationSet{
&Alteration{
AttributeReEncode: &AttributeReEncode{
Attr: &Attribute{Ident: "foo"},
To: &CodecPlain{},
},
},
},
cc: AlterationSet{
&Alteration{
AttributeReEncode: &AttributeReEncode{
Attr: &Attribute{Ident: "foo"},
To: &CodecPlain{},
},
},
},
},
{
name: "not match AttributeReEncode",
aa: AlterationSet{
&Alteration{
AttributeReEncode: &AttributeReEncode{
Attr: &Attribute{Ident: "foo"},
To: &CodecPlain{},
},
},
},
bb: AlterationSet{
&Alteration{
AttributeReEncode: &AttributeReEncode{
Attr: &Attribute{Ident: "foo"},
To: &CodecAlias{Ident: "foo2"},
},
},
},
cc: AlterationSet{
&Alteration{
AttributeReEncode: &AttributeReEncode{
Attr: &Attribute{Ident: "foo"},
To: &CodecPlain{},
},
},
&Alteration{
AttributeReEncode: &AttributeReEncode{
Attr: &Attribute{Ident: "foo"},
To: &CodecAlias{Ident: "foo2"},
},
},
},
},
{
name: "match ModelAdd",
aa: AlterationSet{
&Alteration{
ModelAdd: &ModelAdd{
Model: &Model{Ident: "foo"},
},
},
},
bb: AlterationSet{
&Alteration{
ModelAdd: &ModelAdd{
Model: &Model{Ident: "foo"},
},
},
},
cc: AlterationSet{
&Alteration{
ModelAdd: &ModelAdd{
Model: &Model{Ident: "foo"},
},
},
},
},
{
name: "not match ModelAdd",
aa: AlterationSet{
&Alteration{
ModelAdd: &ModelAdd{
Model: &Model{Ident: "foo"},
},
},
},
bb: AlterationSet{
&Alteration{
ModelAdd: &ModelAdd{
Model: &Model{Ident: "bar"},
},
},
},
cc: AlterationSet{
&Alteration{
ModelAdd: &ModelAdd{
Model: &Model{Ident: "foo"},
},
},
&Alteration{
ModelAdd: &ModelAdd{
Model: &Model{Ident: "bar"},
},
},
},
},
{
name: "match ModelDelete",
aa: AlterationSet{
&Alteration{
ModelDelete: &ModelDelete{
Model: &Model{Ident: "foo"},
},
},
},
bb: AlterationSet{
&Alteration{
ModelDelete: &ModelDelete{
Model: &Model{Ident: "foo"},
},
},
},
cc: AlterationSet{
&Alteration{
ModelDelete: &ModelDelete{
Model: &Model{Ident: "foo"},
},
},
},
},
{
name: "not match ModelDelete",
aa: AlterationSet{
&Alteration{
ModelDelete: &ModelDelete{
Model: &Model{Ident: "foo"},
},
},
},
bb: AlterationSet{
&Alteration{
ModelDelete: &ModelDelete{
Model: &Model{Ident: "bar"},
},
},
},
cc: AlterationSet{
&Alteration{
ModelDelete: &ModelDelete{
Model: &Model{Ident: "foo"},
},
},
&Alteration{
ModelDelete: &ModelDelete{
Model: &Model{Ident: "bar"},
},
},
},
},
}
req := require.New(t)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
cc := tc.aa.Merge(tc.bb)
req.Len(cc, len(tc.cc))
for i, c := range cc {
cmp := c.compare(*tc.cc[i])
req.True(cmp)
}
})
}
}
func TestReTypeMarshling(t *testing.T) {
a := &AttributeReType{
Attr: &Attribute{
Ident: "foo",
Type: &TypeBlob{},
Store: &CodecPlain{},
},
To: &TypeBoolean{},
}
bb, err := json.Marshal(a)
require.NoError(t, err)
b := &AttributeReType{}
err = json.Unmarshal(bb, &b)
require.NoError(t, err)
require.True(t, reflect.DeepEqual(a, b))
}
func TestReEncodeMarshling(t *testing.T) {
a := &AttributeReEncode{
Attr: &Attribute{
Ident: "foo",
Type: &TypeBlob{},
Store: &CodecPlain{},
},
To: &CodecAlias{Ident: "bar"},
}
bb, err := json.Marshal(a)
require.NoError(t, err)
b := &AttributeReEncode{}
err = json.Unmarshal(bb, &b)
require.NoError(t, err)
require.True(t, reflect.DeepEqual(a, b))
}
+3 -3
View File
@@ -180,7 +180,7 @@ func (dd ModelDiffSet) Alterations() (out []*Alteration) {
add(&Alteration{
AttributeReType: &AttributeReType{
Attr: d.Original,
Attr: d.Inserted,
To: d.Inserted.Type,
},
})
@@ -188,8 +188,8 @@ func (dd ModelDiffSet) Alterations() (out []*Alteration) {
case AttributeCodecMismatch:
add(&Alteration{
AttributeReEncode: &AttributeReEncode{
Attr: d.Original,
To: d.Inserted,
Attr: d.Inserted,
To: d.Inserted.Store,
},
})
}
+58
View File
@@ -130,6 +130,17 @@ type (
UUID *TypeUUID `json:"uuid,omitempty"`
}
// auxStoreCodec is a helper struct used for marshaling/unmarshaling
//
// This is required since some fields are interfaces
auxStoreCodec struct {
Type string `json:"type"`
CodecPlain *CodecPlain `json:"codecPlain"`
CodecRecordValueSetJSON *CodecRecordValueSetJSON `json:"codecRecordValueSetJSON"`
CodecAlias *CodecAlias `json:"codecAlias"`
}
AttributeSet []*Attribute
Index struct {
@@ -208,6 +219,36 @@ func (a *Attribute) StoreIdent() string {
}
}
// Compare the two attributes
func (a *Attribute) Compare(b *Attribute) bool {
if a == nil || b == nil {
return a == b
}
out := true
out = out && a.Ident == b.Ident
out = out && a.Label == b.Label
out = out && a.SensitivityLevelID == b.SensitivityLevelID
out = out && a.MultiValue == b.MultiValue
out = out && a.PrimaryKey == b.PrimaryKey
out = out && a.SoftDeleteFlag == b.SoftDeleteFlag
out = out && a.System == b.System
out = out && a.Sortable == b.Sortable
out = out && a.Filterable == b.Filterable
if a.Store == nil || b.Store == nil {
out = out && a.Store == b.Store
} else {
out = out && a.Store.Type() == b.Store.Type()
}
if a.Type == nil || b.Type == nil {
out = out && a.Type == b.Type
} else {
out = out && a.Type.Type() == b.Type.Type()
}
return out
}
func (mm ModelSet) FindByResourceID(resourceID uint64) *Model {
for _, m := range mm {
if m.ResourceID == resourceID {
@@ -347,6 +388,23 @@ func (m Model) Validate() error {
return nil
}
// Compare the two models
//
// This only checks model metadata, the attributes are excluded.
func (a Model) Compare(b Model) bool {
out := true
out = out && a.ConnectionID == b.ConnectionID
out = out && a.Ident == b.Ident
out = out && a.Label == b.Label
out = out && a.Resource == b.Resource
out = out && a.ResourceID == b.ResourceID
out = out && a.ResourceType == b.ResourceType
out = out && a.SensitivityLevelID == b.SensitivityLevelID
return out
}
func (a *Attribute) MarshalJSON() ([]byte, error) {
aux := &auxAttribute{
Ident: a.Ident,
+19
View File
@@ -1,6 +1,8 @@
package dal
import (
"encoding/json"
"reflect"
"testing"
"github.com/stretchr/testify/require"
@@ -59,3 +61,20 @@ func TestModelFindByRefs(t *testing.T) {
})
}
}
func TestAttributeMarshling(t *testing.T) {
a := &Attribute{
Ident: "foo",
Type: &TypeBlob{},
Store: &CodecPlain{},
}
bb, err := json.Marshal(a)
require.NoError(t, err)
b := &Attribute{}
err = json.Unmarshal(bb, &b)
require.NoError(t, err)
require.True(t, reflect.DeepEqual(a, b))
}
+111 -50
View File
@@ -29,13 +29,6 @@ type (
connectionIssues dalIssueIndex
modelIssues dalIssueIndex
Alterations alterations
}
alterations interface {
ModelAlterations(context.Context, *Model) (out []*Alteration, err error)
SetAlterations(ctx context.Context, m *Model, stale []*Alteration, set ...*Alteration) (err error)
}
FullService interface {
@@ -50,7 +43,7 @@ type (
RemoveConnection(ctx context.Context, ID uint64) (err error)
SearchModels(ctx context.Context) (out ModelSet, err error)
ReplaceModel(ctx context.Context, model *Model) (err error)
ReplaceModel(ctx context.Context, currentAlts []*Alteration, model *Model) (newAlts []*Alteration, err error)
RemoveModel(ctx context.Context, connectionID, ID uint64) (err error)
FindModelByResourceID(connectionID uint64, resourceID uint64) *Model
FindModelByResourceIdent(connectionID uint64, resourceType, resourceIdent string) *Model
@@ -73,6 +66,11 @@ type (
var (
gSvc *service
// wrapper around id.Next() that will aid service testing
nextID = func() uint64 {
return id.Next()
}
)
// New creates a DAL service with the primary connection
@@ -682,7 +680,7 @@ func (svc *service) SearchModels(ctx context.Context) (out ModelSet, err error)
// ReplaceModel adds new or updates an existing model
//
// We rely on the user to provide stable and valid model definitions.
func (svc *service) ReplaceModel(ctx context.Context, model *Model) (err error) {
func (svc *service) ReplaceModel(ctx context.Context, currentAlts []*Alteration, model *Model) (newAlts []*Alteration, err error) {
var (
ID = model.ResourceID
connection = svc.GetConnectionByID(model.ConnectionID)
@@ -747,7 +745,7 @@ func (svc *service) ReplaceModel(ctx context.Context, model *Model) (err error)
}
// Add to registry
// @note models should be added to the registry regardless of issues
// Models should be added to the registry regardless of issues
svc.addModelToRegistry(model, upd)
log.Debug(
"added to registry",
@@ -759,48 +757,13 @@ func (svc *service) ReplaceModel(ctx context.Context, model *Model) (err error)
return
}
// Get alterations
// - base from the model diff
df := oldModel.Diff(model)
batchID := id.Next()
aa := df.Alterations()
for _, a := range aa {
a.BatchID = batchID
a.Resource = model.Resource
a.ResourceType = model.ResourceType
a.ConnectionID = model.ConnectionID
}
// - merge with existing
baseAa, err := svc.Alterations.ModelAlterations(ctx, model)
if err != nil {
return
}
// - cleanup to remove duplicates, squash overlapping changes, ...
aa = svc.mergeAlterations(baseAa, aa)
if len(aa) > 0 {
batchID = aa[0].BatchID
}
// - updated with an assertion over the connection
aa, err = connection.connection.AssertSchemaAlterations(ctx, model, aa...)
if err != nil {
return
}
for _, a := range aa {
a.BatchID = batchID
}
err = svc.Alterations.SetAlterations(ctx, model, baseAa, aa...)
newAlts, batchID, err := svc.getSchemaAlterations(ctx, connection, currentAlts, oldModel, model)
if err != nil {
return
}
if len(aa) > 0 {
issues.addModelIssue(model.ResourceID, Issue{
err: errModelRequiresAlteration(connection.ID, model.ResourceID, batchID),
Meta: map[string]any{
"batchID": strconv.FormatUint(batchID, 10),
},
})
if len(newAlts) > 0 {
svc.setAlterationsModelIssue(issues, batchID, connection, model, newAlts)
log.Info("not adding to store: alterations required", zap.Error(err))
return
}
@@ -853,9 +816,9 @@ func (svc *service) ApplyAlteration(ctx context.Context, alts ...*Alteration) (e
return nil, fmt.Errorf("connection not found")
}
model := svc.getModelByRef(ModelRef{Resource: resource, ResourceType: resourceType})
model := svc.getModelByRef(ModelRef{Resource: resource, ResourceType: resourceType, ConnectionID: connectionID})
if model == nil {
return nil, fmt.Errorf("model not found xd")
return nil, fmt.Errorf("model not found")
}
issues = issues.addModel(model.ResourceID)
@@ -867,6 +830,61 @@ func (svc *service) ApplyAlteration(ctx context.Context, alts ...*Alteration) (e
return connection.connection.ApplyAlteration(ctx, model, alts...), nil
}
func (svc *service) ReloadModel(ctx context.Context, currentAlts []*Alteration, model *Model) (newAlts []*Alteration, err error) {
var (
issues = newIssueHelper()
log = svc.logger.Named("models").With(
logger.Uint64("ID", model.ResourceID),
zap.String("ident", model.Ident),
zap.Any("label", model.Label),
)
)
defer svc.updateIssues(issues)
connection := svc.GetConnectionByID(model.ConnectionID)
if connection == nil {
err = fmt.Errorf("connection not found")
return
}
issues = issues.addModel(model.ResourceID)
// @todo consider adding some logging to validators
svc.validateModel(issues, connection, model, model)
svc.validateAttributes(issues, model, model.Attributes...)
// If there are any issues at this stage, there is nothing for us to do
if issues.hasConnectionIssues() || issues.hasModelIssues() {
log.Warn(
"not reloading due to issues",
zap.Any("connection issues", svc.SearchConnectionIssues(model.ConnectionID)),
zap.Any("model issues", svc.SearchModelIssues(model.ResourceID)),
)
return
}
// Re-evaluate schema alterations
// oldModel nil will force it to re-check the entire thing
newAlts, batchID, err := svc.getSchemaAlterations(ctx, connection, currentAlts, nil, model)
if err != nil {
return
}
if len(newAlts) > 0 {
svc.setAlterationsModelIssue(issues, batchID, connection, model, newAlts)
log.Info("not adding to store: alterations required", zap.Error(err))
return
}
err = connection.connection.UpdateModel(ctx, model, model)
if err != nil {
log.Error("failed with errors", zap.Error(err))
}
return
}
// RemoveModel removes the given model from DAL
//
// @todo potentially add more interaction with the connection as in letting it know a model was removed.
@@ -1249,3 +1267,46 @@ func (svc *service) mergeAlterations(base, added AlterationSet) (out AlterationS
return base.Merge(added)
}
func (svc *service) getSchemaAlterations(ctx context.Context, connection *ConnectionWrap, currentAlts []*Alteration, oldModel, model *Model) (newAlts []*Alteration, batchID uint64, err error) {
// - use the diff between the two models as a starting point to see what we should do to support the change
df := oldModel.Diff(model)
newAlts = df.Alterations()
batchID = nextID()
for _, a := range newAlts {
a.BatchID = batchID
a.Resource = model.Resource
a.ResourceType = model.ResourceType
a.ConnectionID = model.ConnectionID
}
// - merge stale and new alteration
// @todo for now we're just using the newly calculated alterations as merging with
// existing ones is not that trivial and doesn't add much value.
// @note this merging assumes the two sets are already ok, valid, and without any
// duplications.
newAlts = svc.mergeAlterations(currentAlts, newAlts)
// - run the alterations against the database to take the schema into consideration
newAlts, err = connection.connection.AssertSchemaAlterations(ctx, model, newAlts...)
if err != nil {
return
}
// - set all of the alterations to the same batch ID
for _, a := range newAlts {
a.BatchID = batchID
}
return
}
func (svc *service) setAlterationsModelIssue(issues *issueHelper, batchID uint64, connection *ConnectionWrap, model *Model, alts []*Alteration) {
if len(alts) > 0 {
issues.addModelIssue(model.ResourceID, Issue{
err: errModelRequiresAlteration(connection.ID, model.ResourceID, batchID),
Meta: map[string]any{
"batchID": strconv.FormatUint(batchID, 10),
},
})
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ type (
Delete(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
Update(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
ReplaceModel(context.Context, *dal.Model) error
ReplaceModel(ctx context.Context, currentAlts []*dal.Alteration, model *dal.Model) (newAlts []*dal.Alteration, err error)
GetConnectionByID(uint64) *dal.ConnectionWrap
}
@@ -343,12 +343,12 @@ func (n *composeModule) Encode(ctx context.Context, pl *payload) (err error) {
// @todo validate ident with connection's ident validator
if err = pl.dal.ReplaceModel(ctx, rModel); err != nil {
if _, err = pl.dal.ReplaceModel(ctx, nil, rModel); err != nil {
return
}
}
if err = pl.dal.ReplaceModel(ctx, model); err != nil {
if _, err = pl.dal.ReplaceModel(ctx, nil, model); err != nil {
return
}
+143 -59
View File
@@ -2,6 +2,7 @@ package service
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
intAuth "github.com/cortezaproject/corteza/server/pkg/auth"
@@ -23,7 +24,10 @@ type (
}
dalAltManager interface {
GetConnectionByID(ID uint64) *dal.ConnectionWrap
ApplyAlteration(ctx context.Context, alts ...*dal.Alteration) (errs []error, err error)
ReloadModel(ctx context.Context, currentAlts []*dal.Alteration, model *dal.Model) (newAlts []*dal.Alteration, err error)
FindModelByResourceIdent(connectionID uint64, resourceType, resourceIdent string) *dal.Model
}
dalSchemaAlterationAccessController interface {
@@ -198,81 +202,92 @@ func (svc dalSchemaAlteration) ModelAlterations(ctx context.Context, m *dal.Mode
return
}
// @todo we can probably do some diffing here to make it more efficient; it'll do for now
func (svc dalSchemaAlteration) SetAlterations(ctx context.Context, m *dal.Model, stale []*dal.Alteration, aa ...*dal.Alteration) (err error) {
func (svc dalSchemaAlteration) SetAlterations(ctx context.Context, s store.Storer, m *dal.Model, stale []*dal.Alteration, aa ...*dal.Alteration) (err error) {
if len(stale)+len(aa) == 0 {
return
}
var (
n = *now()
u = intAuth.GetIdentityFromContext(ctx).Identity()
)
// @todo boilerplate code around this
// @todo this won't work entirely; if someone defines a dal connection to the same DSN as the primary one,
// they can easily bypass this.
// We'll need to do some checking on the DSN; potentially when defining the connection itself.
c := svc.dal.GetConnectionByID(0)
if m.ConnectionID == c.ID && m.Ident == "compose_record" {
err = fmt.Errorf("cannot set alterations for default schema")
return
}
return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
// Firstly get rid of the old ones
aux := make([]*types.DalSchemaAlteration, len(stale))
for i, a := range stale {
aux[i] = &types.DalSchemaAlteration{
ID: a.ID,
}
// Delete current ones
// @todo we might be able to do some diffing to preserve the metadata/ids
// but for now this should be just fine.
auxStale := make([]*types.DalSchemaAlteration, len(stale))
for i, a := range stale {
auxStale[i] = &types.DalSchemaAlteration{
ID: a.ID,
}
err = store.DeleteDalSchemaAlteration(ctx, s, aux...)
if err != nil {
return
}
err = store.DeleteDalSchemaAlteration(ctx, s, auxStale...)
if err != nil {
return
}
// Set new ones
cvt := make(types.DalSchemaAlterationSet, len(aa))
for i, a := range aa {
t := &types.DalSchemaAlteration{
ID: a.ID,
BatchID: a.BatchID,
DependsOn: a.DependsOn,
ConnectionID: a.ConnectionID,
Resource: a.Resource,
ResourceType: a.ResourceType,
Params: &types.DalSchemaAlterationParams{},
}
// Now add the new ones
cvt := make(types.DalSchemaAlterationSet, len(aa))
n := *now()
u := intAuth.GetIdentityFromContext(ctx).Identity()
for i, a := range aa {
t := &types.DalSchemaAlteration{
ID: a.ID,
BatchID: a.BatchID,
DependsOn: a.DependsOn,
ConnectionID: a.ConnectionID,
Resource: a.Resource,
ResourceType: a.ResourceType,
t.ID = nextID()
t.CreatedAt = n
t.CreatedBy = u
Params: &types.DalSchemaAlterationParams{},
}
switch {
case a.AttributeAdd != nil:
t.Kind = "attributeAdd"
t.Params.AttributeAdd = a.AttributeAdd
// @todo we'd need to merge this with the old one probably just to preserve the metadata
t.ID = nextID()
t.CreatedAt = n
t.CreatedBy = u
case a.AttributeDelete != nil:
t.Kind = "attributeDelete"
t.Params.AttributeDelete = a.AttributeDelete
switch {
case a.AttributeAdd != nil:
t.Kind = "attributeAdd"
t.Params.AttributeAdd = a.AttributeAdd
case a.AttributeReType != nil:
t.Kind = "attributeReType"
t.Params.AttributeReType = a.AttributeReType
case a.AttributeDelete != nil:
t.Kind = "attributeDelete"
t.Params.AttributeDelete = a.AttributeDelete
case a.AttributeReEncode != nil:
t.Kind = "attributeReEncode"
t.Params.AttributeReEncode = a.AttributeReEncode
case a.AttributeReType != nil:
t.Kind = "attributeReType"
t.Params.AttributeReType = a.AttributeReType
case a.ModelAdd != nil:
t.Kind = "modelAdd"
t.Params.ModelAdd = a.ModelAdd
case a.AttributeReEncode != nil:
t.Kind = "attributeReEncode"
t.Params.AttributeReEncode = a.AttributeReEncode
case a.ModelDelete != nil:
t.Kind = "modelDelete"
t.Params.ModelDelete = a.ModelDelete
case a.ModelAdd != nil:
t.Kind = "modelAdd"
t.Params.ModelAdd = a.ModelAdd
case a.ModelDelete != nil:
t.Kind = "modelDelete"
t.Params.ModelDelete = a.ModelDelete
}
cvt[i] = t
default:
panic(fmt.Sprintf("unknown alteration type %v", a))
}
return store.UpsertDalSchemaAlteration(ctx, svc.store, cvt...)
})
cvt[i] = t
}
return store.UpsertDalSchemaAlteration(ctx, svc.store, cvt...)
}
func (svc dalSchemaAlteration) Apply(ctx context.Context, ids ...uint64) (err error) {
@@ -285,12 +300,13 @@ func (svc dalSchemaAlteration) Apply(ctx context.Context, ids ...uint64) (err er
return
}
aa, err := svc.toPkgAlterations(ctx, svc.appliableAlterations(aux...)...)
alts := svc.appliableAlterations(aux...)
pkgAlts, err := svc.toPkgAlterations(ctx, alts...)
if err != nil {
return
}
errors, err := svc.dal.ApplyAlteration(ctx, aa...)
errors, err := svc.dal.ApplyAlteration(ctx, pkgAlts...)
if err != nil {
return
}
@@ -304,7 +320,12 @@ func (svc dalSchemaAlteration) Apply(ctx context.Context, ids ...uint64) (err er
}
}
return store.UpdateDalSchemaAlteration(ctx, svc.store, aux...)
err = store.UpdateDalSchemaAlteration(ctx, svc.store, aux...)
if err != nil {
return
}
return svc.reloadAlteredModels(ctx, svc.store, alts)
}
func (svc dalSchemaAlteration) Dismiss(ctx context.Context, ids ...uint64) (err error) {
@@ -327,7 +348,12 @@ func (svc dalSchemaAlteration) Dismiss(ctx context.Context, ids ...uint64) (err
a.DismissedBy = intAuth.GetIdentityFromContext(ctx).Identity()
}
return store.UpdateDalSchemaAlteration(ctx, s, alt...)
err = store.UpdateDalSchemaAlteration(ctx, s, alt...)
if err != nil {
return
}
return svc.reloadAlteredModels(ctx, s, alt)
})
}
@@ -407,3 +433,61 @@ func (svc dalSchemaAlteration) toPkgAlterations(ctx context.Context, aa ...*type
return
}
func (svc dalSchemaAlteration) reloadAlteredModels(ctx context.Context, s store.Storer, alts types.DalSchemaAlterationSet) (err error) {
if len(alts) == 0 {
return
}
// @todo consider lifting this constraint and handle arbitrary sets of alterations
for _, a := range alts {
if a.Resource != alts[0].Resource {
panic("reloadAlteredModels requires all alterations to be for the same resource")
}
}
alt := alts[0]
// Fetch current alterations to see if there are any left over
_, f, err := store.SearchDalSchemaAlterations(ctx, s, types.DalSchemaAlterationFilter{
Resource: []string{alt.Resource},
Deleted: filter.StateExcluded,
Completed: filter.StateExcluded,
Dismissed: filter.StateExcluded,
Paging: filter.Paging{
IncTotal: true,
},
})
if err != nil {
return err
}
// There are some alterations left so we can't reload the model
if f.Total > 0 {
return
}
model := svc.dal.FindModelByResourceIdent(alt.ConnectionID, alt.ResourceType, alt.Resource)
if model == nil {
err = fmt.Errorf("cannot find model for resource %s", alt.Resource)
return
}
currentAlts, err := svc.ModelAlterations(ctx, model)
if err != nil {
return
}
newAlts, err := svc.dal.ReloadModel(ctx, currentAlts, model)
if err != nil {
return
}
err = svc.SetAlterations(ctx, s, model, currentAlts, newAlts...)
if err != nil {
return
}
return
}
@@ -0,0 +1,84 @@
package service
import (
"reflect"
"testing"
"github.com/cortezaproject/corteza/server/system/types"
)
func TestAppliableAlterations(t *testing.T) {
n := now()
tcc := []struct {
name string
in types.DalSchemaAlterationSet
out types.DalSchemaAlterationSet
}{
{
name: "empty",
in: types.DalSchemaAlterationSet{},
out: types.DalSchemaAlterationSet{},
},
{
name: "filter out completed",
in: types.DalSchemaAlterationSet{
{},
{CompletedAt: n},
},
out: types.DalSchemaAlterationSet{
{},
},
},
{
name: "filter out dismissed",
in: types.DalSchemaAlterationSet{
{},
{DismissedAt: n},
},
out: types.DalSchemaAlterationSet{
{},
},
},
{
name: "filter out when dependency on missing",
in: types.DalSchemaAlterationSet{
{},
{DependsOn: 1},
},
out: types.DalSchemaAlterationSet{
{},
},
},
{
name: "include when dependency present",
in: types.DalSchemaAlterationSet{
{ID: 1},
{DependsOn: 1},
},
out: types.DalSchemaAlterationSet{
{ID: 1},
{DependsOn: 1},
},
},
{
name: "include when dependency present but excluded",
in: types.DalSchemaAlterationSet{
{ID: 1, CompletedAt: n, DismissedAt: n},
{DependsOn: 1},
},
out: types.DalSchemaAlterationSet{
{DependsOn: 1},
},
},
}
svc := dalSchemaAlteration{}
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
got := svc.appliableAlterations(tc.in...)
if !reflect.DeepEqual(got, tc.out) {
t.Errorf("appliableAlterations() = %v, want %v", got, tc.out)
}
})
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ type (
DalSchemaAlteration struct {
ID uint64 `json:"alterationID,string"`
BatchID uint64 `json:"batchID,string"`
DependsOn uint64 `json:"dependsOn,string"`
DependsOn uint64 `json:"dependsOn,string,omitempty"`
ConnectionID uint64 `json:"connectionID,string"`
Resource string `json:"resource"`
ResourceType string `json:"resourceType"`