3
0

Introduce DAL model ident formatter to simplify the interfaces

This commit is contained in:
Tomaž Jerman 2022-05-26 17:41:15 +02:00
parent fb7e9b5ce1
commit a0a6314ea9
6 changed files with 171 additions and 71 deletions

View File

@ -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,

View File

@ -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
}

View File

@ -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 != ""
}

View File

@ -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 {

View File

@ -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

View File

@ -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).