Post testing tweaks and fixes
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user