diff --git a/app/boot_levels.go b/app/boot_levels.go index ff23bf2ae..88ee860c8 100644 --- a/app/boot_levels.go +++ b/app/boot_levels.go @@ -336,7 +336,9 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { // DB_DSN is the default connection with full capabilities primaryDalConnection.Config.Connection, dal.ConnectionMeta{ - ConnectionDefaults: primaryDalConnection.ConnectionDefaults(), + DefaultModelIdent: primaryDalConnection.Config.DefaultModelIdent, + DefaultAttributeIdent: primaryDalConnection.Config.DefaultAttributeIdent, + DefaultPartitionFormat: primaryDalConnection.Config.DefaultPartitionFormat, // @todo make it configurable from env SensitivityLevel: 0, Label: primaryDalConnection.Handle, diff --git a/compose/service/module_dal.go b/compose/service/module_dal.go index fbca0d3ab..a0e3c3410 100644 --- a/compose/service/module_dal.go +++ b/compose/service/module_dal.go @@ -14,7 +14,7 @@ import ( type ( dalDDL interface { - ConnectionDefaults(ctx context.Context, connectionID uint64) (dft dal.ConnectionDefaults, err error) + ModelIdentFormatter(connectionID uint64) (f *dal.IdentFormatter, err error) ReloadModel(ctx context.Context, models ...*dal.Model) (err error) AddModel(ctx context.Context, models ...*dal.Model) (err error) @@ -100,16 +100,14 @@ func (svc *module) ReloadDALModels(ctx context.Context) (err error) { } func (svc *module) moduleToModel(ctx context.Context, ns *types.Namespace, mod *types.Module) (*dal.Model, error) { - ccfg, err := svc.dal.ConnectionDefaults(ctx, mod.ModelConfig.ConnectionID) + formatter, tplParts, err := svc.prepareModelFormatter(ns, mod) if err != nil { return nil, err } - getCodec := moduleFieldCodecBuilder(mod.ModelConfig.Partitioned, ccfg) // Metadata out := &dal.Model{ ConnectionID: mod.ModelConfig.ConnectionID, - Ident: svc.formatPartitionIdent(ns, mod, ccfg), Label: mod.Handle, Attributes: make(dal.AttributeSet, len(mod.Fields)), @@ -121,6 +119,14 @@ func (svc *module) moduleToModel(ctx context.Context, ns *types.Namespace, mod * Resource: mod.RbacResource(), } + var ok bool + out.Ident, ok = formatter.ModelIdent(ctx, mod.ModelConfig.Partitioned, mod.ModelConfig.PartitionFormat, tplParts...) + if !ok { + return nil, fmt.Errorf("invalid model identifier generated: %s", out.Ident) + } + + getCodec := moduleFieldCodecBuilder(mod.ModelConfig.Partitioned, formatter) + // Handle user-defined fields for i, f := range mod.Fields { out.Attributes[i], err = svc.moduleFieldToAttribute(getCodec, ns, mod, f) @@ -286,43 +292,18 @@ func (svc *module) moduleFieldToAttribute(getCodec func(f *types.ModuleField) da return } -func (svc *module) formatPartitionIdent(ns *types.Namespace, mod *types.Module, cfg dal.ConnectionDefaults) string { - if !mod.ModelConfig.Partitioned { - return cfg.ModelIdent - } - - pfmt := mod.ModelConfig.PartitionFormat - if pfmt == "" { - pfmt = cfg.PartitionFormat - } - if pfmt == "" { - // @todo put in config or something - pfmt = "compose_record_{{namespace}}_{{module}}" - } - - // @note we must not use name here since it is translatable - mh, _ := handle.Cast(nil, mod.Handle, strconv.FormatUint(mod.ID, 10)) - nsh, _ := handle.Cast(nil, ns.Slug, strconv.FormatUint(ns.ID, 10)) - rpl := strings.NewReplacer( - "{{module}}", mh, - "{{namespace}}", nsh, - ) - - return rpl.Replace(pfmt) -} - -func moduleFieldCodecBuilder(partitioned bool, cfg dal.ConnectionDefaults) func(f *types.ModuleField) dal.Codec { +func moduleFieldCodecBuilder(partitioned bool, formatter *dal.IdentFormatter) func(f *types.ModuleField) dal.Codec { return func(f *types.ModuleField) dal.Codec { - return moduleFieldCodec(f, partitioned, cfg) + return moduleFieldCodec(f, partitioned, formatter) } } -func moduleFieldCodec(f *types.ModuleField, partitioned bool, cfg dal.ConnectionDefaults) (strat dal.Codec) { +func moduleFieldCodec(f *types.ModuleField, partitioned bool, formatter *dal.IdentFormatter) (strat dal.Codec) { if partitioned { strat = &dal.CodecPlain{} } else { - ident := cfg.AttributeIdent - if ident == "" { + ident, ok := formatter.AttributeIdent(partitioned, f.Name) + if !ok { // @todo put in configs or something ident = "values" } @@ -361,3 +342,19 @@ func (svc *module) addModuleToDAL(ctx context.Context, ns *types.Namespace, mod return } + +func (svc *module) prepareModelFormatter(ns *types.Namespace, mod *types.Module) (formatter *dal.IdentFormatter, tplParts []string, err error) { + formatter, err = svc.dal.ModelIdentFormatter(mod.ModelConfig.ConnectionID) + if err != nil { + return + } + + modHandle, _ := handle.Cast(nil, mod.Handle, strconv.FormatUint(mod.ID, 10)) + nsHandle, _ := handle.Cast(nil, ns.Slug, strconv.FormatUint(ns.ID, 10)) + tplParts = []string{ + "module", modHandle, + "namespace", nsHandle, + } + + return +} diff --git a/pkg/dal/model.go b/pkg/dal/model.go index 1a93974e1..829665cbc 100644 --- a/pkg/dal/model.go +++ b/pkg/dal/model.go @@ -1,14 +1,27 @@ package dal import ( + "context" "fmt" "strings" + "github.com/PaesslerAG/gval" "github.com/cortezaproject/corteza-server/pkg/handle" "github.com/modern-go/reflect2" ) type ( + IdentFormatter struct { + defaultModelIdent string + defaultAttributeIdent string + + defaultPartitionFormat string + + things map[string]any + + partitionFormatValidator gval.Evaluable + } + // ModelFilter is used to retrieve a model from the DAL based on given params ModelFilter struct { ConnectionID uint64 @@ -181,3 +194,84 @@ func (m Model) Validate() error { return nil } + +func (f *IdentFormatter) AddEvalParam(params map[string]any) { + if f.things == nil { + f.things = make(map[string]any) + } + + for k, v := range params { + f.things[k] = v + } + +} + +func (f IdentFormatter) getEvalParams(ident string) (out map[string]any) { + out = map[string]any{ + "ident": ident, + } + + for k, v := range f.things { + out[k] = v + } + + return +} + +func (f IdentFormatter) ModelIdent(ctx context.Context, partitioned bool, tpl string, kv ...string) (out string, ok bool) { + ok = true + + if !partitioned { + return f.defaultModelIdent, ok + } + + // A bit of preprocessing + { + if len(kv)%2 != 0 { + panic("ModelIdentFormatter.ModelIdent requires key/value pairs") + } + for i := 0; i < len(kv); i += 2 { + kv[i] = fmt.Sprintf("{{%s}}", kv[i]) + } + } + + // Template preparation with defaulting based on provided KV + { + if tpl == "" { + tpl = f.defaultPartitionFormat + } + if tpl == "" { + pts := make([]string, 0, len(kv)/2) + for i := 0; i < len(kv); i += 2 { + pts = append(pts, kv[i]) + } + tpl = strings.Join(pts, "_") + } + if tpl == "" { + return "", false + } + } + + rpl := strings.NewReplacer(kv...) + out = rpl.Replace(tpl) + + if f.partitionFormatValidator != nil { + var err error + ok, err = f.partitionFormatValidator.EvalBool(ctx, f.getEvalParams(out)) + ok = ok && (err == nil) + } + + return +} + +func (f IdentFormatter) AttributeIdent(partitioned bool, ident string) (out string, ok bool) { + if !partitioned { + if f.defaultAttributeIdent != "" { + return f.defaultAttributeIdent, true + } + return "", false + } + + return ident, ident != "" + +} diff --git a/pkg/dal/service.go b/pkg/dal/service.go index 66bd818e5..880201087 100644 --- a/pkg/dal/service.go +++ b/pkg/dal/service.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/cortezaproject/corteza-server/pkg/dal/capabilities" + "github.com/cortezaproject/corteza-server/pkg/expr" "github.com/cortezaproject/corteza-server/pkg/filter" "go.uber.org/zap" ) @@ -16,21 +17,19 @@ type ( sensitivityLevel uint64 connection Connection - Defaults ConnectionDefaults + meta ConnectionMeta } ConnectionMeta struct { - ConnectionDefaults - SensitivityLevel uint64 Label string - } - ConnectionDefaults struct { - ModelIdent string - AttributeIdent string + DefaultModelIdent string + DefaultAttributeIdent string - PartitionFormat string + DefaultPartitionFormat string + + PartitionValidator string } service struct { @@ -71,7 +70,7 @@ func InitGlobalService(ctx context.Context, log *zap.Logger, inDev bool, cp Conn var err error cw := &connectionWrap{ - Defaults: cm.ConnectionDefaults, + meta: cm, sensitivityLevel: cm.SensitivityLevel, label: cm.Label, } @@ -134,7 +133,7 @@ func (svc *service) AddConnection(ctx context.Context, connectionID uint64, cp C cw := &connectionWrap{ connectionID: connectionID, - Defaults: cm.ConnectionDefaults, + meta: cm, sensitivityLevel: cm.SensitivityLevel, label: cm.Label, } @@ -183,16 +182,6 @@ func (svc *service) UpdateConnection(ctx context.Context, connectionID uint64, c return svc.AddConnection(ctx, connectionID, cp, cm, capabilities...) } -// ConnectionDefaultreturns the defaults we can use with this connection -func (svc *service) ConnectionDefaults(ctx context.Context, connectionID uint64) (dft ConnectionDefaults, err error) { - wrap, _, err := svc.getConnection(ctx, connectionID) - if err != nil { - return - } - - return wrap.Defaults, nil -} - // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // @@ -280,6 +269,34 @@ func (svc *service) ReloadModel(ctx context.Context, models ...*Model) (err erro return svc.AddModel(ctx, models...) } +func (svc *service) ModelIdentFormatter(connectionID uint64) (f *IdentFormatter, err error) { + // @todo ... + c := svc.connections[connectionID] + if connectionID == 0 { + c = svc.primary + } + + if c == nil { + err = fmt.Errorf("connection %d does not exist", connectionID) + return + } + + f = &IdentFormatter{ + defaultModelIdent: c.meta.DefaultModelIdent, + defaultAttributeIdent: c.meta.DefaultAttributeIdent, + defaultPartitionFormat: c.meta.DefaultPartitionFormat, + } + + if c.meta.PartitionValidator != "" { + f.partitionFormatValidator, err = expr.Parser().NewEvaluable(c.meta.PartitionValidator) + if err != nil { + return + } + } + + return +} + // AddModel adds support for a new model func (svc *service) AddModel(ctx context.Context, models ...*Model) (err error) { svc.logger.Debug("adding model", zap.Int("count", len(models))) @@ -412,7 +429,7 @@ func (svc *service) modelByConnection(models ModelSet) (out map[uint64]ModelSet) func (svc *service) registerModel(ctx context.Context, cw *connectionWrap, connectionID uint64, models ModelSet) (err error) { for _, model := range models { - svc.logger.Debug("adding model for connection", zap.Uint64("connectionID", connectionID), zap.String("resource type", model.ResourceType), zap.String("resource model", model.Resource)) + svc.logger.Debug("adding model for connection", zap.Uint64("connectionID", connectionID), zap.String("resource type", model.ResourceType), zap.String("resource model", model.Resource), zap.String("model ident", model.Ident)) existing := svc.GetModelByResource(connectionID, model.ResourceType, model.Resource) if existing != nil { diff --git a/system/service/dal_connection.go b/system/service/dal_connection.go index 20891e2ea..43513640c 100644 --- a/system/service/dal_connection.go +++ b/system/service/dal_connection.go @@ -296,13 +296,11 @@ func (svc *dalConnection) reloadConnections(ctx context.Context) (err error) { func (svc *dalConnection) makeConnectionMeta(ctx context.Context, c *types.DalConnection) (cm dal.ConnectionMeta, err error) { // @todo we could probably utilize connection params more here cm = dal.ConnectionMeta{ - ConnectionDefaults: dal.ConnectionDefaults{ - ModelIdent: c.Config.DefaultModelIdent, - AttributeIdent: c.Config.DefaultAttributeIdent, - PartitionFormat: c.Config.DefaultPartitionFormat, - }, - SensitivityLevel: c.SensitivityLevel, - Label: c.Handle, + DefaultModelIdent: c.Config.DefaultModelIdent, + DefaultAttributeIdent: c.Config.DefaultAttributeIdent, + DefaultPartitionFormat: c.Config.DefaultPartitionFormat, + SensitivityLevel: c.SensitivityLevel, + Label: c.Handle, } return diff --git a/system/types/dal_connection.go b/system/types/dal_connection.go index 7706ed6c9..d6140cb3a 100644 --- a/system/types/dal_connection.go +++ b/system/types/dal_connection.go @@ -78,14 +78,6 @@ var ( DalPrimaryConnectionResourceType = "corteza::system:primary_dal_connection" ) -func (c DalConnection) ConnectionDefaults() dal.ConnectionDefaults { - return dal.ConnectionDefaults{ - ModelIdent: c.Config.DefaultModelIdent, - AttributeIdent: c.Config.DefaultAttributeIdent, - PartitionFormat: c.Config.DefaultPartitionFormat, - } -} - func (c DalConnection) ActiveCapabilities() capabilities.Set { return c.Capabilities.Supported. Union(c.Capabilities.Enforced).